diff --git a/QMUI/.github/ISSUE_TEMPLATE/bug_report.md b/QMUI/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 1f3a44f1..00000000 --- a/QMUI/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Bug -about: QMUIKit 框架本身的 bug(请注意区分非 QMUIKit 代码引发的问题) -title: '' -labels: 'bug' -assignees: '' - ---- - -**Bug 表现** -问题的具体描述 - -**截图** -Bug 现场的界面截图,或者 Xcode 控制台的错误信息截图,有问题的代码截图 - -**如何重现** -1. ... -2. ... - -**预期的表现** -正常情况下,应该是什么表现 - -**其他信息** - - 设备: [例如模拟器、iPhone、iPad] - - iOS 版本: [iOS 12.x] - - Xcode 版本: [Xcode 10.x] - - QMUI 版本: [3.x.x] diff --git a/QMUI/.github/ISSUE_TEMPLATE/feature_request.md b/QMUI/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 98765b71..00000000 --- a/QMUI/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: 意见与建议 -about: 功能、接口设计等的相关建议 -title: '' -labels: 'suggest' -assignees: '' - ---- - -**现存问题或期望目标** -对于功能的建议,请说明具体的场景,现在的代码为什么无法实现需求。 -对于代码设计方面的建议,请说明目前的问题所在。 diff --git a/QMUI/.gitignore b/QMUI/.gitignore index 82ed296f..1d74e0bf 100644 --- a/QMUI/.gitignore +++ b/QMUI/.gitignore @@ -1,4 +1,4 @@ .DS_Store -xcuserdata/ - +.xcuserdatad +xcuserdata diff --git a/QMUI/LICENSE.TXT b/QMUI/LICENSE.TXT index b6931290..485d8bd0 100644 --- a/QMUI/LICENSE.TXT +++ b/QMUI/LICENSE.TXT @@ -1,5 +1,5 @@ Tencent is pleased to support the open source community by making QMUI_iOS available. -Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. +Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. If you have downloaded a copy of the QMUI_iOS binary from Tencent, please note that the QMUI_iOS binary is licensed under the MIT License. If you have downloaded a copy of the QMUI_iOS source code from Tencent, please note that QMUI_iOS source code is licensed under the MIT License. Your integration of QMUI_iOS into your own projects may require compliance with the MIT License. A copy of the MIT License is included in this file. diff --git a/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.h b/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.h index b102380c..543a96f4 100644 --- a/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.h +++ b/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIConfigurationTemplate.h diff --git a/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m b/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m index 52ada5e7..74e36837 100644 --- a/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m +++ b/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIConfigurationTemplate.m @@ -42,7 +42,7 @@ - (void)applyConfigurationTemplate { QMUICMI.backgroundColor = nil; // UIColorForBackground : 界面背景色,默认用于 QMUICommonViewController.view 的背景色 QMUICMI.maskDarkColor = UIColorMakeWithRGBA(0, 0, 0, .35f); // UIColorMask : 深色的背景遮罩,默认用于 QMAlertController、QMUIDialogViewController 等弹出控件的遮罩 QMUICMI.maskLightColor = UIColorMakeWithRGBA(255, 255, 255, .5f); // UIColorMaskWhite : 浅色的背景遮罩,QMUIKit 里默认没用到,只是占个位 - QMUICMI.separatorColor = UIColorMake(222, 224, 226); // UIColorSeparator : 全局默认的分割线颜色,默认用于列表分隔线颜色、UIView (QMUI_Border) 分隔线颜色 + QMUICMI.separatorColor = UIColorMake(222, 224, 226); // UIColorSeparator : 全局默认的分割线颜色,默认用于列表分隔线颜色、UIView (QMUIBorder) 分隔线颜色 QMUICMI.separatorDashedColor = UIColorMake(17, 17, 17); // UIColorSeparatorDashed : 全局默认的虚线分隔线的颜色,默认 QMUIKit 暂时没用到 QMUICMI.placeholderColor = UIColorMake(196, 200, 208); // UIColorPlaceholder,全局的输入框的 placeholder 颜色,默认用于 QMUITextField、QMUITextView,不影响系统 UIKit 的输入框 @@ -51,6 +51,11 @@ - (void)applyConfigurationTemplate { QMUICMI.testColorGreen = UIColorMakeWithRGBA(0, 255, 0, .3); QMUICMI.testColorBlue = UIColorMakeWithRGBA(0, 0, 255, .3); + #pragma mark - QMUILog + QMUICMI.shouldPrintDefaultLog = YES; // ShouldPrintDefaultLog : 是否允许输出 QMUILogLevelDefault 级别的 log + QMUICMI.shouldPrintInfoLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelInfo 级别的 log + QMUICMI.shouldPrintWarnLog = YES; // ShouldPrintWarnLog : 是否允许输出 QMUILogLevelWarn 级别的 log + QMUICMI.shouldPrintQMUIWarnLogToConsole = NO; // ShouldPrintQMUIWarnLogToConsole : 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上 #pragma mark - UIControl @@ -62,36 +67,40 @@ - (void)applyConfigurationTemplate { QMUICMI.buttonDisabledAlpha = UIControlDisabledAlpha; // ButtonDisabledAlpha : QMUIButton 在 disabled 时的 alpha,不影响系统的 UIButton QMUICMI.buttonTintColor = UIColorBlue; // ButtonTintColor : QMUIButton 默认的 tintColor,不影响系统的 UIButton - QMUICMI.ghostButtonColorBlue = UIColorBlue; // GhostButtonColorBlue : QMUIGhostButtonColorBlue 的颜色 - QMUICMI.ghostButtonColorRed = UIColorRed; // GhostButtonColorRed : QMUIGhostButtonColorRed 的颜色 - QMUICMI.ghostButtonColorGreen = UIColorGreen; // GhostButtonColorGreen : QMUIGhostButtonColorGreen 的颜色 - QMUICMI.ghostButtonColorGray = UIColorGray; // GhostButtonColorGray : QMUIGhostButtonColorGray 的颜色 - QMUICMI.ghostButtonColorWhite = UIColorWhite; // GhostButtonColorWhite : QMUIGhostButtonColorWhite 的颜色 - - QMUICMI.fillButtonColorBlue = UIColorBlue; // FillButtonColorBlue : QMUIFillButtonColorBlue 的颜色 - QMUICMI.fillButtonColorRed = UIColorRed; // FillButtonColorRed : QMUIFillButtonColorRed 的颜色 - QMUICMI.fillButtonColorGreen = UIColorGreen; // FillButtonColorGreen : QMUIFillButtonColorGreen 的颜色 - QMUICMI.fillButtonColorGray = UIColorGray; // FillButtonColorGray : QMUIFillButtonColorGray 的颜色 - QMUICMI.fillButtonColorWhite = UIColorWhite; // FillButtonColorWhite : QMUIFillButtonColorWhite 的颜色 - - #pragma mark - TextField & TextView + #pragma mark - TextInput + QMUICMI.textFieldTextColor = nil; // TextFieldTextColor : QMUITextField、QMUITextView 的 textColor,不影响 UIKit 的输入框 QMUICMI.textFieldTintColor = nil; // TextFieldTintColor : QMUITextField、QMUITextView 的 tintColor,不影响 UIKit 的输入框 QMUICMI.textFieldTextInsets = UIEdgeInsetsMake(0, 7, 0, 7); // TextFieldTextInsets : QMUITextField 的内边距,不影响 UITextField + QMUICMI.keyboardAppearance = UIKeyboardAppearanceDefault; // KeyboardAppearance : UITextView、UITextField、UISearchBar 的 keyboardAppearance + + #pragma mark - UISwitch + QMUICMI.switchOnTintColor = nil; // SwitchOnTintColor : UISwitch 打开时的背景色(除了圆点外的其他颜色) + QMUICMI.switchOffTintColor = nil; // SwitchOffTintColor : UISwitch 关闭时的背景色(除了圆点外的其他颜色) + QMUICMI.switchThumbTintColor = nil; // SwitchThumbTintColor : UISwitch 中间的操控圆点的颜色 #pragma mark - NavigationBar + if (@available(iOS 15.0, *)) { + QMUICMI.navBarUsesStandardAppearanceOnly = NO; // NavBarUsesStandardAppearanceOnly : 对于 iOS 15 的系统,UINavigationBar 的样式分为滚动前和滚动后,虽然系统的注释里说了如果没设置 scrollEdgeAppearance 则会用 standardAppearance 代替,但实际运行效果是 scrollEdgeAppearance 默认并不会保持与 standardAppearance 一致,所以这里提供一个开关,允许你在打开开关时让 QMUI 帮你同步 standardAppearance 的值,以使 App 保持与 iOS 14 相同的效果。如需打开该开关,请保证在其他 NavBar 开关之前设置。 + } + QMUICMI.navBarContainerClasses = nil; // NavBarContainerClasses : NavigationBar 系列开关被用于 UIAppearance 时的生效范围(默认情况下除了用于 UIAppearance 外,还用于实现了 QMUINavigationControllerAppearanceDelegate 的 UIViewController),默认为 nil。当赋值为 nil 或者空数组时等效于 @[UINavigationController.class],也即对所有 UINavigationBar 生效,包括系统的通讯录(ContactsUI.framework)、打印等。当值不为空时,获取 UINavigationBar 的 appearance 请使用 UINavigationBar.qmui_appearanceConfigured 方法代替系统的 UINavigationBar.appearance。请保证这个配置项先于其他任意 NavBar 配置项执行。 QMUICMI.navBarHighlightedAlpha = 0.2f; // NavBarHighlightedAlpha : QMUINavigationButton 在 highlighted 时的 alpha QMUICMI.navBarDisabledAlpha = 0.2f; // NavBarDisabledAlpha : QMUINavigationButton 在 disabled 时的 alpha - QMUICMI.navBarButtonFont = nil; // NavBarButtonFont : QMUINavigationButtonTypeNormal 和 UINavigationBar 上的 UIBarButtonItem 的字体 - QMUICMI.navBarButtonFontBold = nil; // NavBarButtonFontBold : QMUINavigationButtonTypeBold 的字体 + QMUICMI.navBarButtonFont = nil; // NavBarButtonFont : UINavigationBar 里 UIBarButtonItem 以及 QMUINavigationButtonTypeNormal 的字体 + QMUICMI.navBarButtonFontBold = nil; // NavBarButtonFontBold : iOS 15 及以后用于设置 UINavigationBar 里 Done 类型的 UIBarButtonItem 以及 QMUINavigationButtonTypeBold 的字体,iOS 14 及以前只对后者生效 QMUICMI.navBarBackgroundImage = nil; // NavBarBackgroundImage : UINavigationBar 的背景图 - QMUICMI.navBarShadowImage = nil; // NavBarShadowImage : UINavigationBar.shadowImage,也即导航栏底部那条分隔线 + if (@available(iOS 15.0, *)) { + QMUICMI.navBarRemoveBackgroundEffectAutomatically = NO; // NavBarRemoveBackgroundEffectAutomatically : iOS 15 及以后,QMUI 里的 UINavigationBar 使用的是 UINavigationBarAppearance 来设置样式,新方式默认是 backgroundImage 和 backgroundEffect 共存的,而 iOS 14 及以前的旧方式,一旦设置了 backgroundImage 则 backgroundEffect 自动会被移除,因此提供该开关允许业务将行为回退到 iOS 14 及以前的效果。默认为 NO。 + } + QMUICMI.navBarShadowImage = nil; // NavBarShadowImage : UINavigationBar.shadowImage,也即导航栏底部那条分隔线,配合 NavBarShadowImageColor 使用。 + QMUICMI.navBarShadowImageColor = nil; // NavBarShadowImageColor : UINavigationBar.shadowImage 的颜色,如果为 nil,则使用 NavBarShadowImage 的值,如果 NavBarShadowImage 也为 nil,则使用系统默认的分隔线。如果不为 nil,而 NavBarShadowImage 为 nil,则自动创建一张 1px 高的图并将其设置为 NavBarShadowImageColor 的颜色然后设置上去,如果 NavBarShadowImage 不为 nil 且 renderingMode 不为 UIImageRenderingModeAlwaysOriginal,则将 NavBarShadowImage 设置为 NavBarShadowImageColor 的颜色然后设置上去。 QMUICMI.navBarBarTintColor = nil; // NavBarBarTintColor : UINavigationBar.barTintColor,也即背景色 - QMUICMI.navBarTintColor = nil; // NavBarTintColor : QMUINavigationController.navigationBar 的 tintColor,也即导航栏上面的按钮颜色,由于 tintColor 不支持 appearance,所以这里只支持 QMUINavigationController + QMUICMI.navBarStyle = UIBarStyleDefault; // NavBarStyle : UINavigationBar 的 barStyle + QMUICMI.navBarTintColor = nil; // NavBarTintColor : NavBarContainerClasses 里的 UINavigationBar 的 tintColor,也即导航栏上面的按钮颜色 QMUICMI.navBarTitleColor = nil; // NavBarTitleColor : UINavigationBar 的标题颜色,以及 QMUINavigationTitleView 的默认文字颜色 QMUICMI.navBarTitleFont = nil; // NavBarTitleFont : UINavigationBar 的标题字体,以及 QMUINavigationTitleView 的默认字体 - QMUICMI.navBarLargeTitleColor = nil; // NavBarLargeTitleColor : UINavigationBar 在大标题模式下的标题颜色,仅在 iOS 11 之后才有效 - QMUICMI.navBarLargeTitleFont = nil; // NavBarLargeTitleFont : UINavigationBar 在大标题模式下的标题字体,仅在 iOS 11 之后才有效 + QMUICMI.navBarLargeTitleColor = nil; // NavBarLargeTitleColor : UINavigationBar 在大标题模式下的标题颜色 + QMUICMI.navBarLargeTitleFont = nil; // NavBarLargeTitleFont : UINavigationBar 在大标题模式下的标题字体 QMUICMI.navBarBackButtonTitlePositionAdjustment = UIOffsetZero; // NavBarBarBackButtonTitlePositionAdjustment : 导航栏返回按钮的文字偏移 QMUICMI.sizeNavBarBackIndicatorImageAutomatically = YES; // SizeNavBarBackIndicatorImageAutomatically : 是否要自动调整 NavBarBackIndicatorImage 的 size 为 (13, 21) QMUICMI.navBarBackIndicatorImage = nil; // NavBarBackIndicatorImage : 导航栏的返回按钮的图片,图片尺寸建议为(13, 21),否则最终的图片位置无法与系统原生的位置保持一致 @@ -104,46 +113,62 @@ - (void)applyConfigurationTemplate { #pragma mark - TabBar + if (@available(iOS 15.0, *)) { + QMUICMI.tabBarUsesStandardAppearanceOnly = NO; // TabBarUsesStandardAppearanceOnly : 对于 iOS 15 的系统,UITabBar 的样式分为滚动前和滚动后,虽然系统的注释里说了如果没设置 scrollEdgeAppearance 则会用 standardAppearance 代替,但实际运行效果是 scrollEdgeAppearance 默认并不会保持与 standardAppearance 一致,所以这里提供一个开关,允许你在打开开关时让 QMUI 帮你同步 standardAppearance 的值,以使 App 保持与 iOS 14 相同的效果。如需打开该开关,请保证在其他 NavBar 开关之前设置。 + } + QMUICMI.tabBarContainerClasses = nil; // TabBarContainerClasses : TabBar 系列开关的生效范围,默认为 nil,当赋值为 nil 或者空数组时等效于 @[UITabBarController.class],也即对所有 UITabBar 生效。当值不为空时,获取 UITabBar 的 appearance 请使用 UITabBar.qmui_appearanceConfigured 方法代替系统的 UITabBar.appearance。请保证这个配置项先于其他任意 TabBar 配置项执行。 QMUICMI.tabBarBackgroundImage = nil; // TabBarBackgroundImage : UITabBar 的背景图 - QMUICMI.tabBarBarTintColor = nil; // TabBarBarTintColor : UITabBar 的 barTintColor + if (@available(iOS 15.0, *)) { + QMUICMI.tabBarRemoveBackgroundEffectAutomatically = NO; // TabBarRemoveBackgroundEffectAutomatically : iOS 15 及以后,QMUI 里的 UITabBar 使用的是 UITabBarAppearance 来设置样式,新方式默认是 backgroundImage 和 backgroundEffect 共存的,而 iOS 14 及以前的旧方式,一旦设置了 backgroundImage 则 backgroundEffect 自动会被移除,因此提供该开关允许业务将行为回退到 iOS 14 及以前的效果。默认为 NO。 + } + QMUICMI.tabBarBarTintColor = nil; // TabBarBarTintColor : UITabBar 的 barTintColor,如果需要看到磨砂效果则应该提供半透明的色值 QMUICMI.tabBarShadowImageColor = nil; // TabBarShadowImageColor : UITabBar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片 - QMUICMI.tabBarTintColor = nil; // TabBarTintColor : UITabBar 的 tintColor - QMUICMI.tabBarItemTitleColor = nil; // TabBarItemTitleColor : 未选中的 UITabBarItem 的标题颜色 - QMUICMI.tabBarItemTitleColorSelected = TabBarTintColor; // TabBarItemTitleColorSelected : 选中的 UITabBarItem 的标题颜色 + QMUICMI.tabBarStyle = UIBarStyleDefault; // TabBarStyle : UITabBar 的 barStyle QMUICMI.tabBarItemTitleFont = nil; // TabBarItemTitleFont : UITabBarItem 的标题字体 + QMUICMI.tabBarItemTitleFontSelected = nil; // TabBarItemTitleFontSelected : 选中的 UITabBarItem 的标题字体 + QMUICMI.tabBarItemTitleColor = nil; // TabBarItemTitleColor : 未选中的 UITabBarItem 的标题颜色 + QMUICMI.tabBarItemTitleColorSelected = nil; // TabBarItemTitleColorSelected : 选中的 UITabBarItem 的标题颜色 + QMUICMI.tabBarItemImageColor = nil; // TabBarItemImageColor : UITabBarItem 未选中时的图片颜色 + QMUICMI.tabBarItemImageColorSelected = nil; // TabBarItemImageColorSelected : UITabBarItem 选中时的图片颜色 #pragma mark - Toolbar + if (@available(iOS 15.0, *)) { + QMUICMI.toolBarUsesStandardAppearanceOnly = NO; // ToolBarUsesStandardAppearanceOnly : 对于 iOS 15 的系统,UIToolbar 的样式分为滚动前和滚动后,虽然系统的注释里说了如果没设置 scrollEdgeAppearance 则会用 standardAppearance 代替,但实际运行效果是 scrollEdgeAppearance 默认并不会保持与 standardAppearance 一致,所以这里提供一个开关,允许你在打开开关时让 QMUI 帮你同步 standardAppearance 的值,以使 App 保持与 iOS 14 相同的效果。如需打开该开关,请保证在其他 NavBar 开关之前设置。 + } + QMUICMI.toolBarContainerClasses = nil; // ToolBarContainerClasses : ToolBar 系列开关的生效范围,默认为 nil,当赋值为 nil 或者空数组时等效于 @[UINavigationController.class],也即对所有 UIToolbar 生效。当值不为空时,获取 UIToolbar 的 appearance 请使用 UIToolbar.qmui_appearanceConfigured 方法代替系统的 UIToolbar.appearance。请保证这个配置项先于其他任意 ToolBar 配置项执行。 QMUICMI.toolBarHighlightedAlpha = 0.4f; // ToolBarHighlightedAlpha : QMUIToolbarButton 在 highlighted 状态下的 alpha QMUICMI.toolBarDisabledAlpha = 0.4f; // ToolBarDisabledAlpha : QMUIToolbarButton 在 disabled 状态下的 alpha - QMUICMI.toolBarTintColor = nil; // ToolBarTintColor : UIToolbar 的 tintColor,以及 QMUIToolbarButton normal 状态下的文字颜色 + QMUICMI.toolBarTintColor = nil; // ToolBarTintColor : NavBarContainerClasses 里的 UIToolbar 的 tintColor,以及 QMUIToolbarButton normal 状态下的文字颜色 QMUICMI.toolBarTintColorHighlighted = [ToolBarTintColor colorWithAlphaComponent:ToolBarHighlightedAlpha]; // ToolBarTintColorHighlighted : QMUIToolbarButton 在 highlighted 状态下的文字颜色 QMUICMI.toolBarTintColorDisabled = [ToolBarTintColor colorWithAlphaComponent:ToolBarDisabledAlpha]; // ToolBarTintColorDisabled : QMUIToolbarButton 在 disabled 状态下的文字颜色 - QMUICMI.toolBarBackgroundImage = nil; // ToolBarBackgroundImage : UIToolbar 的背景图 - QMUICMI.toolBarBarTintColor = nil; // ToolBarBarTintColor : UIToolbar 的 tintColor - QMUICMI.toolBarShadowImageColor = nil; // ToolBarShadowImageColor : UIToolbar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片 + QMUICMI.toolBarBackgroundImage = nil; // ToolBarBackgroundImage : NavBarContainerClasses 里的 UIToolbar 的背景图 + if (@available(iOS 15.0, *)) { + QMUICMI.toolBarRemoveBackgroundEffectAutomatically = NO; // ToolBarRemoveBackgroundEffectAutomatically : iOS 15 及以后,QMUI 里的 UIToolbar 使用的是 UIToolbarAppearance 来设置样式,新方式默认是 backgroundImage 和 backgroundEffect 共存的,而 iOS 14 及以前的旧方式,一旦设置了 backgroundImage 则 backgroundEffect 自动会被移除,因此提供该开关允许业务将行为回退到 iOS 14 及以前的效果。默认为 NO。 + } + QMUICMI.toolBarBarTintColor = nil; // ToolBarBarTintColor : NavBarContainerClasses 里的 UIToolbar 的 tintColor + QMUICMI.toolBarShadowImageColor = nil; // ToolBarShadowImageColor : NavBarContainerClasses 里的 UIToolbar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片 + QMUICMI.toolBarStyle = UIBarStyleDefault; // ToolBarStyle : NavBarContainerClasses 里的 UIToolbar 的 barStyle QMUICMI.toolBarButtonFont = nil; // ToolBarButtonFont : QMUIToolbarButton 的字体 #pragma mark - SearchBar - QMUICMI.searchBarTextFieldBackground = nil; // SearchBarTextFieldBackground : QMUISearchBar 里的文本框的背景颜色 + QMUICMI.searchBarTextFieldBackgroundImage = nil; // SearchBarTextFieldBackgroundImage : QMUISearchBar 里的文本框的背景图,图片高度会决定输入框的高度 QMUICMI.searchBarTextFieldBorderColor = nil; // SearchBarTextFieldBorderColor : QMUISearchBar 里的文本框的边框颜色 - QMUICMI.searchBarBottomBorderColor = nil; // SearchBarBottomBorderColor : QMUISearchBar 底部分隔线颜色 - QMUICMI.searchBarBarTintColor = nil; // SearchBarBarTintColor : QMUISearchBar 的 barTintColor,也即背景色 + QMUICMI.searchBarTextFieldCornerRadius = 2.0; // SearchBarTextFieldCornerRadius : QMUISearchBar 里的文本框的圆角大小,-1 表示圆角大小为输入框高度的一半 + QMUICMI.searchBarBackgroundImage = nil; // SearchBarBackgroundImage : 搜索框的背景图,如果需要设置底部分隔线的颜色也请绘制到图片里 QMUICMI.searchBarTintColor = nil; // SearchBarTintColor : QMUISearchBar 的 tintColor,也即上面的操作控件的主题色 QMUICMI.searchBarTextColor = nil; // SearchBarTextColor : QMUISearchBar 里的文本框的文字颜色 QMUICMI.searchBarPlaceholderColor = UIColorPlaceholder; // SearchBarPlaceholderColor : QMUISearchBar 里的文本框的 placeholder 颜色 QMUICMI.searchBarFont = nil; // SearchBarFont : QMUISearchBar 里的文本框的文字字体及 placeholder 的字体 QMUICMI.searchBarSearchIconImage = nil; // SearchBarSearchIconImage : QMUISearchBar 里的放大镜 icon QMUICMI.searchBarClearIconImage = nil; // SearchBarClearIconImage : QMUISearchBar 里的文本框输入文字时右边的清空按钮的图片 - QMUICMI.searchBarTextFieldCornerRadius = 2.0; // SearchBarTextFieldCornerRadius : QMUISearchBar 里的文本框的圆角大小,-1 表示圆角大小为输入框高度的一半 - #pragma mark - TableView / TableViewCell + #pragma mark - Plain TableView QMUICMI.tableViewEstimatedHeightEnabled = YES; // TableViewEstimatedHeightEnabled : 是否要开启全局 UITableView 的 estimatedRow(Section/Footer)Height QMUICMI.tableViewBackgroundColor = nil; // TableViewBackgroundColor : Plain 类型的 QMUITableView 的背景色颜色 - QMUICMI.tableViewGroupedBackgroundColor = nil; // TableViewGroupedBackgroundColor : Grouped 类型的 QMUITableView 的背景色 QMUICMI.tableSectionIndexColor = nil; // TableSectionIndexColor : 列表右边的字母索引条的文字颜色 QMUICMI.tableSectionIndexBackgroundColor = nil; // TableSectionIndexBackgroundColor : 列表右边的字母索引条的背景色 QMUICMI.tableSectionIndexTrackingBackgroundColor = nil; // TableSectionIndexTrackingBackgroundColor : 列表右边的字母索引条在选中时的背景色 @@ -152,7 +177,7 @@ - (void)applyConfigurationTemplate { QMUICMI.tableViewCellNormalHeight = UITableViewAutomaticDimension; // TableViewCellNormalHeight : QMUITableView 的默认 cell 高度 QMUICMI.tableViewCellTitleLabelColor = nil; // TableViewCellTitleLabelColor : QMUITableViewCell 的 textLabel 的文字颜色 QMUICMI.tableViewCellDetailLabelColor = nil; // TableViewCellDetailLabelColor : QMUITableViewCell 的 detailTextLabel 的文字颜色 - QMUICMI.tableViewCellBackgroundColor = UIColorWhite; // TableViewCellBackgroundColor : QMUITableViewCell 的背景色 + QMUICMI.tableViewCellBackgroundColor = nil; // TableViewCellBackgroundColor : QMUITableViewCell 的背景色 QMUICMI.tableViewCellSelectedBackgroundColor = UIColorMake(238, 239, 241); // TableViewCellSelectedBackgroundColor : QMUITableViewCell 点击时的背景色 QMUICMI.tableViewCellWarningBackgroundColor = UIColorYellow; // TableViewCellWarningBackgroundColor : QMUITableViewCell 用于表示警告时的背景色,备用 QMUICMI.tableViewCellDisclosureIndicatorImage = nil; // TableViewCellDisclosureIndicatorImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryDisclosureIndicator 时的箭头的图片 @@ -170,7 +195,18 @@ - (void)applyConfigurationTemplate { QMUICMI.tableViewSectionFooterAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewSectionFooterAccessoryMargins : Plain 类型的 QMUITableView sectionFooter accessoryView 的间距 QMUICMI.tableViewSectionHeaderContentInset = UIEdgeInsetsMake(4, 15, 4, 15); // TableViewSectionHeaderContentInset : Plain 类型的 QMUITableView sectionHeader 里的内容的 padding QMUICMI.tableViewSectionFooterContentInset = UIEdgeInsetsMake(4, 15, 4, 15); // TableViewSectionFooterContentInset : Plain 类型的 QMUITableView sectionFooter 里的内容的 padding - + if (@available(iOS 15, *)) { + QMUICMI.tableViewSectionHeaderTopPadding = UITableViewAutomaticDimension; // TableViewSectionHeaderTopPadding : Plain 类型的 QMUITableView 在 iOS 15 上的 sectionHeaderTopPadding 值,仅当存在 sectionHeader 时才有效,系统的默认值为 UITableViewAutomaticDimension,表现出来是22pt的空隙 + } + + #pragma mark - Grouped TableView + QMUICMI.tableViewGroupedBackgroundColor = nil; // TableViewGroupedBackgroundColor : Grouped 类型的 QMUITableView 的背景色 + QMUICMI.tableViewGroupedSeparatorColor = TableViewSeparatorColor; // TableViewGroupedSeparatorColor : Grouped 类型的 QMUITableView 分隔线颜色 + QMUICMI.tableViewGroupedCellTitleLabelColor = TableViewCellTitleLabelColor; // TableViewGroupedCellTitleLabelColor : Grouped 类型的 QMUITableView cell 里的标题颜色 + QMUICMI.tableViewGroupedCellDetailLabelColor = TableViewCellDetailLabelColor; // TableViewGroupedCellDetailLabelColor : Grouped 类型的 QMUITableView cell 里的副标题颜色 + QMUICMI.tableViewGroupedCellBackgroundColor = TableViewCellBackgroundColor; // TableViewGroupedCellBackgroundColor : Grouped 类型的 QMUITableView cell 背景色 + QMUICMI.tableViewGroupedCellSelectedBackgroundColor = TableViewCellSelectedBackgroundColor; // TableViewGroupedCellSelectedBackgroundColor : Grouped 类型的 QMUITableView cell 点击时的背景色 + QMUICMI.tableViewGroupedCellWarningBackgroundColor = TableViewCellWarningBackgroundColor; // tableViewGroupedCellWarningBackgroundColor : Grouped 类型的 QMUITableView cell 在提醒状态下的背景色 QMUICMI.tableViewGroupedSectionHeaderFont = UIFontMake(12); // TableViewGroupedSectionHeaderFont : Grouped 类型的 QMUITableView sectionHeader 里的文字字体 QMUICMI.tableViewGroupedSectionFooterFont = UIFontMake(12); // TableViewGroupedSectionFooterFont : Grouped 类型的 QMUITableView sectionFooter 里的文字字体 QMUICMI.tableViewGroupedSectionHeaderTextColor = UIColorGrayDarken; // TableViewGroupedSectionHeaderTextColor : Grouped 类型的 QMUITableView sectionHeader 里的文字颜色 @@ -181,43 +217,68 @@ - (void)applyConfigurationTemplate { QMUICMI.tableViewGroupedSectionFooterDefaultHeight = UITableViewAutomaticDimension; // TableViewGroupedSectionFooterDefaultHeight : Grouped 类型的 QMUITableView sectionFooter 的默认高度(也即没使用自定义的 sectionFooterView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN QMUICMI.tableViewGroupedSectionHeaderContentInset = UIEdgeInsetsMake(16, 15, 8, 15); // TableViewGroupedSectionHeaderContentInset : Grouped 类型的 QMUITableView sectionHeader 里的内容的 padding QMUICMI.tableViewGroupedSectionFooterContentInset = UIEdgeInsetsMake(8, 15, 2, 15); // TableViewGroupedSectionFooterContentInset : Grouped 类型的 QMUITableView sectionFooter 里的内容的 padding + if (@available(iOS 15, *)) { + QMUICMI.tableViewGroupedSectionHeaderTopPadding = UITableViewAutomaticDimension; // TableViewGroupedSectionHeaderTopPadding : Grouped 类型的 QMUITableView 在 iOS 15 上的 sectionHeaderTopPadding 值,仅当存在 sectionHeader 时才有效,系统的默认值为 UITableViewAutomaticDimension,表现出来是0。 + } + + #pragma mark - InsetGrouped TableView + QMUICMI.tableViewInsetGroupedCornerRadius = 10; // TableViewInsetGroupedCornerRadius : InsetGrouped 类型的 UITableView 内 cell 的圆角值 + QMUICMI.tableViewInsetGroupedHorizontalInset = PreferredValueForVisualDevice(20, 15); // TableViewInsetGroupedHorizontalInset: InsetGrouped 类型的 UITableView 内的左右缩进值 + QMUICMI.tableViewInsetGroupedBackgroundColor = TableViewGroupedBackgroundColor; // TableViewInsetGroupedBackgroundColor : InsetGrouped 类型的 UITableView 的背景色 + QMUICMI.tableViewInsetGroupedSeparatorColor = TableViewGroupedSeparatorColor; // TableViewInsetGroupedSeparatorColor : InsetGrouped 类型的 QMUITableView 分隔线颜色 + QMUICMI.tableViewInsetGroupedCellTitleLabelColor = TableViewGroupedCellTitleLabelColor; // TableViewInsetGroupedCellTitleLabelColor : InsetGrouped 类型的 QMUITableView cell 里的标题颜色 + QMUICMI.tableViewInsetGroupedCellDetailLabelColor = TableViewGroupedCellDetailLabelColor; // TableViewInsetGroupedCellDetailLabelColor : InsetGrouped 类型的 QMUITableView cell 里的副标题颜色 + QMUICMI.tableViewInsetGroupedCellBackgroundColor = TableViewGroupedCellBackgroundColor; // TableViewInsetGroupedCellBackgroundColor : InsetGrouped 类型的 QMUITableView cell 背景色 + QMUICMI.tableViewInsetGroupedCellSelectedBackgroundColor = TableViewGroupedCellSelectedBackgroundColor; // TableViewInsetGroupedCellSelectedBackgroundColor : InsetGrouped 类型的 QMUITableView cell 点击时的背景色 + QMUICMI.tableViewInsetGroupedCellWarningBackgroundColor = TableViewGroupedCellWarningBackgroundColor; // TableViewInsetGroupedCellWarningBackgroundColor : InsetGrouped 类型的 QMUITableView cell 在提醒状态下的背景色 + QMUICMI.tableViewInsetGroupedSectionHeaderFont = TableViewGroupedSectionHeaderFont; // TableViewInsetGroupedSectionHeaderFont : InsetGrouped 类型的 QMUITableView sectionHeader 里的文字字体 + QMUICMI.tableViewInsetGroupedSectionFooterFont = TableViewInsetGroupedSectionHeaderFont; // TableViewInsetGroupedSectionFooterFont : InsetGrouped 类型的 QMUITableView sectionFooter 里的文字字体 + QMUICMI.tableViewInsetGroupedSectionHeaderTextColor = TableViewGroupedSectionHeaderTextColor; // TableViewInsetGroupedSectionHeaderTextColor : InsetGrouped 类型的 QMUITableView sectionHeader 里的文字颜色 + QMUICMI.tableViewInsetGroupedSectionFooterTextColor = TableViewInsetGroupedSectionHeaderTextColor; // TableViewInsetGroupedSectionFooterTextColor : InsetGrouped 类型的 QMUITableView sectionFooter 里的文字颜色 + QMUICMI.tableViewInsetGroupedSectionHeaderAccessoryMargins = TableViewGroupedSectionHeaderAccessoryMargins; // TableViewInsetGroupedSectionHeaderAccessoryMargins : InsetGrouped 类型的 QMUITableView sectionHeader accessoryView 的间距 + QMUICMI.tableViewInsetGroupedSectionFooterAccessoryMargins = TableViewInsetGroupedSectionHeaderAccessoryMargins; // TableViewInsetGroupedSectionFooterAccessoryMargins : InsetGrouped 类型的 QMUITableView sectionFooter accessoryView 的间距 + QMUICMI.tableViewInsetGroupedSectionHeaderDefaultHeight = TableViewGroupedSectionHeaderDefaultHeight; // TableViewInsetGroupedSectionHeaderDefaultHeight : InsetGrouped 类型的 QMUITableView sectionHeader 的默认高度(也即没使用自定义的 sectionHeaderView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN + QMUICMI.tableViewInsetGroupedSectionFooterDefaultHeight = TableViewGroupedSectionFooterDefaultHeight; // TableViewInsetGroupedSectionFooterDefaultHeight : InsetGrouped 类型的 QMUITableView sectionFooter 的默认高度(也即没使用自定义的 sectionFooterView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN + QMUICMI.tableViewInsetGroupedSectionHeaderContentInset = TableViewGroupedSectionHeaderContentInset; // TableViewInsetGroupedSectionHeaderContentInset : InsetGrouped 类型的 QMUITableView sectionHeader 里的内容的 padding + QMUICMI.tableViewInsetGroupedSectionFooterContentInset = TableViewInsetGroupedSectionHeaderContentInset; // TableViewInsetGroupedSectionFooterContentInset : InsetGrouped 类型的 QMUITableView sectionFooter 里的内容的 padding + if (@available(iOS 15, *)) { + QMUICMI.tableViewInsetGroupedSectionHeaderTopPadding = UITableViewAutomaticDimension; // TableViewInsetGroupedSectionHeaderTopPadding : InsetGrouped 类型的 QMUITableView 在 iOS 15 上的 sectionHeaderTopPadding 值,仅当存在 sectionHeader 时才有效,系统的默认值为 UITableViewAutomaticDimension,表现出来是0。 + } #pragma mark - UIWindowLevel QMUICMI.windowLevelQMUIAlertView = UIWindowLevelAlert - 4.0; // UIWindowLevelQMUIAlertView : QMUIModalPresentationViewController、QMUIPopupContainerView 里使用的 UIWindow 的 windowLevel - - #pragma mark - QMUILog - QMUICMI.shouldPrintDefaultLog = YES; // ShouldPrintDefaultLog : 是否允许输出 QMUILogLevelDefault 级别的 log - QMUICMI.shouldPrintInfoLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelInfo 级别的 log - QMUICMI.shouldPrintWarnLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelWarn 级别的 log + QMUICMI.windowLevelQMUIConsole = 1; // UIWindowLevelQMUIConsole : QMUIConsole 内部的 UIWindow 的 windowLevel #pragma mark - QMUIBadge - QMUICMI.badgeBackgroundColor = UIColorRed; // BadgeBackgroundColor : UIBarButtoItem、UITabBarItem 上的未读数的背景色 - QMUICMI.badgeTextColor = UIColorWhite; // BadgeTextColor : UIBarButtoItem、UITabBarItem 上的未读数的文字颜色 - QMUICMI.badgeFont = UIFontBoldMake(11); // BadgeFont : UIBarButtoItem、UITabBarItem 上的未读数的字体 - QMUICMI.badgeContentEdgeInsets = UIEdgeInsetsMake(2, 4, 2, 4); // BadgeContentEdgeInsets : UIBarButtoItem、UITabBarItem 上的未读数与圆圈之间的 padding - QMUICMI.badgeCenterOffset = CGPointMake(10, -9); // BadgeCenterOffset : UIBarButtoItem、UITabBarItem 上的未读数相对于 item 中心的偏移 - QMUICMI.badgeCenterOffsetLandscape = CGPointMake(10, -9); // BadgeCenterOffsetLandscape : UIBarButtoItem、UITabBarItem 上的未读数在横屏下相对于 item 中心的偏移 + QMUICMI.badgeBackgroundColor = UIColorRed; // BadgeBackgroundColor : QMUIBadge 上的未读数的背景色 + QMUICMI.badgeTextColor = UIColorWhite; // BadgeTextColor : QMUIBadge 上的未读数的文字颜色 + QMUICMI.badgeFont = UIFontBoldMake(11); // BadgeFont : QMUIBadge 上的未读数的字体 + QMUICMI.badgeContentEdgeInsets = UIEdgeInsetsMake(2, 4, 2, 4); // BadgeContentEdgeInsets : QMUIBadge 上的未读数与圆圈之间的 padding + QMUICMI.badgeOffset = CGPointMake(-9, 11); // BadgeOffset : QMUIBadge 上的未读数相对于目标 view 右上角的偏移 + QMUICMI.badgeOffsetLandscape = CGPointMake(-9, 6); // BadgeOffsetLandscape : QMUIBadge 上的未读数在横屏下相对于目标 view 右上角的偏移 - QMUICMI.updatesIndicatorColor = UIColorRed; // UpdatesIndicatorColor : UIBarButtoItem、UITabBarItem 上的未读红点的颜色 - QMUICMI.updatesIndicatorSize = CGSizeMake(7, 7); // UpdatesIndicatorSize : UIBarButtoItem、UITabBarItem 上的未读红点的大小 - QMUICMI.updatesIndicatorCenterOffset = CGPointMake(10, -9); // UpdatesIndicatorCenterOffset : UIBarButtoItem、UITabBarItem 上的未读红点相对于 item 中心的偏移 - QMUICMI.updatesIndicatorCenterOffsetLandscape = CGPointMake(10, -9); // UpdatesIndicatorCenterOffsetLandscape : UIBarButtoItem、UITabBarItem 上的未读红点在横屏下相对于 item 中心的偏移 + QMUICMI.updatesIndicatorColor = UIColorRed; // UpdatesIndicatorColor : QMUIBadge 上的未读红点的颜色 + QMUICMI.updatesIndicatorSize = CGSizeMake(7, 7); // UpdatesIndicatorSize : QMUIBadge 上的未读红点的大小 + QMUICMI.updatesIndicatorOffset = CGPointMake(4, UpdatesIndicatorSize.height);// UpdatesIndicatorOffset : QMUIBadge 未读红点相对于目标 view 右上角的偏移 + QMUICMI.updatesIndicatorOffsetLandscape = UpdatesIndicatorOffset; // UpdatesIndicatorOffsetLandscape : QMUIBadge 未读红点在横屏下相对于目标 view 右上角的偏移 #pragma mark - Others QMUICMI.automaticCustomNavigationBarTransitionStyle = NO; // AutomaticCustomNavigationBarTransitionStyle : 界面 push/pop 时是否要自动根据两个界面的 barTintColor/backgroundImage/shadowImage 的样式差异来决定是否使用自定义的导航栏效果 QMUICMI.supportedOrientationMask = UIInterfaceOrientationMaskAll; // SupportedOrientationMask : 默认支持的横竖屏方向 - QMUICMI.automaticallyRotateDeviceOrientation = NO; // AutomaticallyRotateDeviceOrientation : 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕 - QMUICMI.statusbarStyleLightInitially = NO; // StatusbarStyleLightInitially : 默认的状态栏内容是否使用白色,默认为 NO,也即黑色 - QMUICMI.needsBackBarButtonItemTitle = NO; // NeedsBackBarButtonItemTitle : 全局是否需要返回按钮的 title,不需要则只显示一个返回image + QMUICMI.automaticallyRotateDeviceOrientation = NO; // AutomaticallyRotateDeviceOrientation : 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕(仅 iOS 15 及以前版本需要,iOS 16 系统会自动处理,该开关无意义。) + QMUICMI.defaultStatusBarStyle = UIStatusBarStyleDefault; // DefaultStatusBarStyle : 默认的状态栏样式,默认值为 UIStatusBarStyleDefault,也即在 iOS 12 及以前是黑色文字,iOS 13 及以后会自动根据当前 App 是否处于 Dark Mode 切换颜色。如果你希望固定为白色,请设置为 UIStatusBarStyleLightContent,固定黑色则设置为 UIStatusBarStyleDarkContent。 + QMUICMI.needsBackBarButtonItemTitle = YES; // NeedsBackBarButtonItemTitle : 全局是否需要返回按钮的 title,不需要则只显示一个返回image QMUICMI.hidesBottomBarWhenPushedInitially = NO; // HidesBottomBarWhenPushedInitially : QMUICommonViewController.hidesBottomBarWhenPushed 的初始值,默认为 NO,以保持与系统默认值一致,但通常建议改为 YES,因为一般只有 tabBar 首页那几个界面要求为 NO QMUICMI.preventConcurrentNavigationControllerTransitions = YES; // PreventConcurrentNavigationControllerTransitions : 自动保护 QMUINavigationController 在上一次 push/pop 尚未结束的时候就进行下一次 push/pop 的行为,避免产生 crash QMUICMI.navigationBarHiddenInitially = NO; // NavigationBarHiddenInitially : QMUINavigationControllerDelegate preferredNavigationBarHidden 的初始值,默认为NO - QMUICMI.shouldFixTabBarTransitionBugInIPhoneX = NO; // ShouldFixTabBarTransitionBugInIPhoneX : 是否需要自动修复 iOS 11 下,iPhone X 的设备在 push 界面时,tabBar 会瞬间往上跳的 bug - QMUICMI.shouldFixTabBarButtonBugForAll = NO; // ShouldFixTabBarButtonBugForAll : 是否要对 iOS 12.1.1 及以后的版本也修复手势返回时 tabBarButton 布局错误的 bug(issue #410),默认为 NO + 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 下正常控制滚动条的位置。 } // QMUI 2.3.0 版本里,配置表新增这个方法,返回 YES 表示在 App 启动时要自动应用这份配置表。仅当你的 App 里存在多份配置表时,才需要把除默认配置表之外的其他配置表的返回值改为 NO。 diff --git a/QMUI/QMUIKit.podspec b/QMUI/QMUIKit.podspec index e27b76eb..aef548fd 100644 --- a/QMUI/QMUIKit.podspec +++ b/QMUI/QMUIKit.podspec @@ -1,35 +1,33 @@ Pod::Spec.new do |s| s.name = "QMUIKit" - s.version = "3.2.0" + s.version = "4.8.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, '9.0' - s.frameworks = 'Foundation', 'UIKit', 'CoreGraphics', 'Photos' + s.platform = :ios, '13.0' + s.frameworks = 'Foundation', 'UIKit', 'CoreGraphics' s.preserve_paths = 'QMUIConfigurationTemplate/*' s.source_files = 'QMUIKit/QMUIKit.h' + s.resource_bundles = {'QMUIKit' => ['QMUIKit/PrivacyInfo.xcprivacy']} s.subspec 'QMUICore' do |ss| - ss.source_files = 'QMUIKit/QMUIKit.h', 'QMUIKit/QMUICore', 'QMUIKit/UIKitExtensions' + ss.source_files = 'QMUIKit/QMUIKit.h', 'QMUIKit/QMUICore', 'QMUIKit/UIKitExtensions', 'QMUIKit/UIKitExtensions/QMUIBarProtocol' + ss.frameworks = 'CoreImage', 'ImageIO' ss.dependency 'QMUIKit/QMUIWeakObjectContainer' ss.dependency 'QMUIKit/QMUILog' end - s.subspec 'QMUIResources' do |ss| - ss.resource = 'QMUIKit/QMUIResources/*.bundle' - end - s.subspec 'QMUIMainFrame' do |ss| ss.source_files = 'QMUIKit/QMUIMainFrame' ss.dependency 'QMUIKit/QMUICore' @@ -42,6 +40,15 @@ Pod::Spec.new do |s| ss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates' end + s.subspec 'QMUIResources' do |ss| + ss.resource_bundles = {'QMUIResources' => ['QMUIKit/QMUIResources/*.*']} + ss.pod_target_xcconfig = { + 'EXPANDED_CODE_SIGN_IDENTITY' => '', + 'CODE_SIGNING_REQUIRED' => 'NO', + 'CODE_SIGNING_ALLOWED' => 'NO', + } + end + s.subspec 'QMUIWeakObjectContainer' do |ss| ss.source_files = 'QMUIKit/QMUIComponents/QMUIWeakObjectContainer.{h,m}' end @@ -59,28 +66,24 @@ Pod::Spec.new do |s| sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates' end - ss.subspec 'QMUINavigationTitleView' do |sss| - sss.source_files = 'QMUIKit/QMUIComponents/QMUINavigationTitleView.{h,m}' - sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' - end - - ss.subspec 'QMUIButton' do |sss| - sss.source_files = 'QMUIKit/QMUIComponents/QMUIButton/QMUIButton.{h,m}' + ss.subspec 'QMUICALayerExtension' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates' end - ss.subspec 'QMUIFillButton' do |sss| - sss.source_files = 'QMUIKit/QMUIComponents/QMUIButton/QMUIFillButton.{h,m}' - sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + ss.subspec 'QMUIAnimation' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIAnimation' end - ss.subspec 'QMUIGhostButton' do |sss| - sss.source_files = 'QMUIKit/QMUIComponents/QMUIButton/QMUIGhostButton.{h,m}' + ss.subspec 'QMUINavigationTitleView' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUINavigationTitleView.{h,m}' sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance' end - ss.subspec 'QMUILinkButton' do |sss| - sss.source_files = 'QMUIKit/QMUIComponents/QMUIButton/QMUILinkButton.{h,m}' - sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + ss.subspec 'QMUIButton' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIButton/QMUIButton.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUILayouter' end ss.subspec 'QMUINavigationButton' do |sss| @@ -104,14 +107,29 @@ Pod::Spec.new do |s| ss.subspec 'QMUIEmptyView' do |sss| sss.source_files = 'QMUIKit/QMUIComponents/QMUIEmptyView.{h,m}' sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance' end ss.subspec 'QMUILabel' do |sss| sss.source_files = 'QMUIKit/QMUIComponents/QMUILabel.{h,m}' end + ss.subspec 'QMUILayouter' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUILayouter/*.{h,m}' + end + + ss.subspec 'QMUISheetPresentation' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUISheetPresentation/*.{h,m}' + sss.dependency 'QMUIKit/QMUIMainFrame' + sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates' + sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + sss.dependency 'QMUIKit/QMUIComponents/QMUINavigationButton' + end + ss.subspec 'QMUIKeyboardManager' do |sss| sss.source_files = 'QMUIKit/QMUIComponents/QMUIKeyboardManager.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance' + sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates' end # 从这里开始就是非必须的组件 @@ -125,6 +143,13 @@ Pod::Spec.new do |s| sss.dependency 'QMUIKit/QMUIComponents/QMUIModalPresentationViewController' sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' sss.dependency 'QMUIKit/QMUIComponents/QMUITextField' + sss.dependency 'QMUIKit/QMUIComponents/QMUIKeyboardManager' + sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance' + sss.dependency 'QMUIKit/QMUIComponents/QMUILabel' + end + + ss.subspec 'QMUIAppearance' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIAppearance.{h,m}' end ss.subspec 'QMUICellHeightCache' do |sss| @@ -145,9 +170,13 @@ Pod::Spec.new do |s| ss.subspec 'QMUIConsole' do |sss| sss.source_files = 'QMUIKit/QMUIComponents/QMUIConsole/*.{h,m}' + sss.dependency 'QMUIKit/QMUIResources' sss.dependency 'QMUIKit/QMUIComponents/QMUITextView' sss.dependency 'QMUIKit/QMUIComponents/QMUITextField' sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + sss.dependency 'QMUIKit/QMUIComponents/QMUITableView' + sss.dependency 'QMUIKit/QMUIComponents/QMUITableViewCell' + sss.dependency 'QMUIKit/QMUIComponents/QMUICellHeightKeyCache' sss.dependency 'QMUIKit/QMUIComponents/QMUIPopupMenuView' sss.dependency 'QMUIKit/QMUIComponents/QMUICAAnimationExtension' end @@ -166,12 +195,13 @@ Pod::Spec.new do |s| sss.dependency 'QMUIKit/QMUIComponents/QMUITextField' sss.dependency 'QMUIKit/QMUIComponents/QMUITableViewCell' sss.dependency 'QMUIKit/QMUIComponents/QMUINavigationTitleView' + sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance' end ss.subspec 'QMUIEmotionView' do |sss| sss.source_files = 'QMUIKit/QMUIComponents/QMUIEmotionView.{h,m}' - sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' sss.dependency 'QMUIKit/QMUIResources' + sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' end ss.subspec 'QMUIFloatLayoutView' do |sss| @@ -189,6 +219,7 @@ Pod::Spec.new do |s| sss.dependency 'QMUIKit/QMUIComponents/QMUIEmptyView' sss.dependency 'QMUIKit/QMUIComponents/QMUIPieProgressView' sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance' sss.dependency 'QMUIKit/QMUIMainFrame' end @@ -199,12 +230,14 @@ Pod::Spec.new do |s| ss.subspec 'QMUIModalPresentationViewController' do |sss| sss.source_files = 'QMUIKit/QMUIComponents/QMUIModalPresentationViewController.{h,m}' sss.dependency 'QMUIKit/QMUIComponents/QMUIKeyboardManager' + sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance' end ss.subspec 'QMUIMoreOperationController' do |sss| sss.source_files = 'QMUIKit/QMUIComponents/QMUIMoreOperationController.{h,m}' sss.dependency 'QMUIKit/QMUIComponents/QMUIModalPresentationViewController' sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance' end ss.subspec 'QMUIOrderedDictionary' do |sss| @@ -218,12 +251,17 @@ Pod::Spec.new do |s| ss.subspec 'QMUIPopupContainerView' do |sss| sss.source_files = 'QMUIKit/QMUIComponents/QMUIPopupContainerView.{h,m}' sss.dependency 'QMUIKit/QMUIMainFrame' + sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance' end ss.subspec 'QMUIPopupMenuView' do |sss| sss.source_files = 'QMUIKit/QMUIComponents/QMUIPopupMenuView/*.{h,m}' sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + sss.dependency 'QMUIKit/QMUIComponents/QMUITableView' + sss.dependency 'QMUIKit/QMUIComponents/QMUILabel' + sss.dependency 'QMUIKit/QMUIComponents/QMUILayouter' sss.dependency 'QMUIKit/QMUIComponents/QMUIPopupContainerView' + sss.dependency 'QMUIKit/QMUIComponents/QMUICheckbox' end ss.subspec 'QMUIScrollAnimator' do |sss| @@ -251,10 +289,6 @@ Pod::Spec.new do |s| sss.source_files = 'QMUIKit/QMUIComponents/QMUISegmentedControl.{h,m}' end - ss.subspec 'QMUISlider' do |sss| - sss.source_files = 'QMUIKit/QMUIComponents/QMUISlider.{h,m}' - end - ss.subspec 'QMUITableViewCell' do |sss| sss.source_files = 'QMUIKit/QMUIComponents/QMUITableViewCell.{h,m}' sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' @@ -279,27 +313,49 @@ Pod::Spec.new do |s| sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates' end + ss.subspec 'QMUITheme' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUITheme/*.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUIImagePickerLibrary' + sss.dependency 'QMUIKit/QMUIComponents/QMUIAlertController' + sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + sss.dependency 'QMUIKit/QMUIComponents/QMUIConsole' + sss.dependency 'QMUIKit/QMUIComponents/QMUIEmotionView' + sss.dependency 'QMUIKit/QMUIComponents/QMUIEmptyView' + sss.dependency 'QMUIKit/QMUIComponents/QMUIGridView' + sss.dependency 'QMUIKit/QMUIComponents/QMUIImagePreviewView' + sss.dependency 'QMUIKit/QMUIComponents/QMUILabel' + sss.dependency 'QMUIKit/QMUIComponents/QMUIPopupContainerView' + sss.dependency 'QMUIKit/QMUIComponents/QMUIPopupMenuView' + sss.dependency 'QMUIKit/QMUIComponents/QMUITextField' + sss.dependency 'QMUIKit/QMUIComponents/QMUITextView' + sss.dependency 'QMUIKit/QMUIComponents/QMUIToastView' + sss.dependency 'QMUIKit/QMUIComponents/QMUIModalPresentationViewController' + sss.dependency 'QMUIKit/QMUIComponents/QMUIBadge' + end + ss.subspec 'QMUITips' do |sss| sss.source_files = 'QMUIKit/QMUIComponents/QMUITips.{h,m}' - sss.dependency 'QMUIKit/QMUIComponents/QMUIToastView' sss.dependency 'QMUIKit/QMUIResources' + sss.dependency 'QMUIKit/QMUIComponents/QMUIToastView' end - ss.subspec 'QMUIVisualEffectView' do |sss| - sss.source_files = 'QMUIKit/QMUIComponents/QMUIVisualEffectView.{h,m}' + ss.subspec 'QMUIWindowSizeMonitor' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.{h,m}' end ss.subspec 'QMUIZoomImageView' do |sss| sss.source_files = 'QMUIKit/QMUIComponents/QMUIZoomImageView.{h,m}' + sss.frameworks = 'PhotosUI', 'CoreMedia', 'AVFoundation', 'QuartzCore' + sss.dependency 'QMUIKit/QMUIResources' sss.dependency 'QMUIKit/QMUIComponents/QMUIEmptyView' sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' - sss.dependency 'QMUIKit/QMUIComponents/QMUISlider' sss.dependency 'QMUIKit/QMUIComponents/QMUIPieProgressView' sss.dependency 'QMUIKit/QMUIComponents/QMUIAssetLibrary' end ss.subspec 'QMUIAssetLibrary' do |sss| sss.source_files = 'QMUIKit/QMUIComponents/AssetLibrary/*.{h,m}' + sss.frameworks = 'Photos', 'CoreServices' end ss.subspec 'QMUIImagePickerLibrary' do |sss| @@ -314,6 +370,7 @@ Pod::Spec.new do |s| sss.dependency 'QMUIKit/QMUIComponents/QMUIZoomImageView' sss.dependency 'QMUIKit/QMUIComponents/QMUIAlertController' sss.dependency 'QMUIKit/QMUIComponents/QMUIEmptyView' + sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance' end @@ -344,7 +401,6 @@ Pod::Spec.new do |s| ss.subspec 'QMUIToastView' do |sss| sss.source_files = 'QMUIKit/QMUIComponents/ToastView/*.{h,m}' sss.dependency 'QMUIKit/QMUIComponents/QMUIKeyboardManager' - sss.dependency 'QMUIKit/QMUIComponents/QMUIVisualEffectView' end ss.subspec 'QMUIStaticTableView' do |sss| @@ -353,6 +409,12 @@ Pod::Spec.new do |s| sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates' end + ss.subspec 'QMUICheckbox' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUICheckbox.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + sss.dependency 'QMUIKit/QMUIResources' + end + end end diff --git a/QMUI/QMUIKit/Info.plist b/QMUI/QMUIKit/Info.plist index 957c532e..ec0cc7b0 100644 --- a/QMUI/QMUIKit/Info.plist +++ b/QMUI/QMUIKit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.2.0 + $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/QMUI/QMUIKit/PrivacyInfo.xcprivacy b/QMUI/QMUIKit/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..0734d308 --- /dev/null +++ b/QMUI/QMUIKit/PrivacyInfo.xcprivacy @@ -0,0 +1,23 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTrackingDomains + + NSPrivacyTracking + + + diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.h b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.h index 67f09886..9277b810 100644 --- a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.h +++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIAsset.h @@ -43,7 +43,7 @@ typedef NS_ENUM(NSUInteger, QMUIAssetDownloadStatus) { /** * 相册里某一个资源的包装对象,该资源可能是图片、视频等。 - * @note QMUIAsset 重写了 isEqual: 方法,只要两个 QMUIAsset 的 adentifier 相同,则认为是同一个对象,以方便在数组、字典等容器中对大量 QMUIAsset 进行遍历查找等操作。 + * @note QMUIAsset 重写了 isEqual: 方法,只要两个 QMUIAsset 的 identifier 相同,则认为是同一个对象,以方便在数组、字典等容器中对大量 QMUIAsset 进行遍历查找等操作。 */ @interface QMUIAsset : NSObject @@ -117,11 +117,11 @@ typedef NS_ENUM(NSUInteger, QMUIAssetDownloadStatus) { * @param completion 完成请求后调用的 block,参数中包含了请求的 Live Photo 以及相关信息,若 assetType 不是 QMUIAssetTypeLivePhoto 则为 nil * @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。 * - * @wraning iOS 9.1 以下中并没有 Live Photo,因此无法获取有效结果。 + * @warning iOS 9.1 以下中并没有 Live Photo,因此无法获取有效结果。 * * @return 返回请求图片的请求 id */ -- (NSInteger)requestLivePhotoWithCompletion:(void (^)(PHLivePhoto *livePhoto, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler NS_AVAILABLE_IOS(9_1); +- (NSInteger)requestLivePhotoWithCompletion:(void (^)(PHLivePhoto *livePhoto, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler; /** * 异步请求 AVPlayerItem,可能会有网络请求 diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.m b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.m index 9f04afcc..0f6b6b7e 100644 --- a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.m +++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIAsset.m @@ -15,7 +15,7 @@ #import "QMUIAsset.h" #import -#import +#import #import "QMUICore.h" #import "QMUIAssetsManager.h" #import "NSString+QMUI.h" @@ -42,15 +42,11 @@ - (instancetype)initWithPHAsset:(PHAsset *)phAsset { switch (phAsset.mediaType) { case PHAssetMediaTypeImage: _assetType = QMUIAssetTypeImage; - if ([[phAsset valueForKey:@"uniformTypeIdentifier"] isEqualToString:(__bridge NSString *)kUTTypeGIF]) { + if ([[phAsset qmui_valueForKey:@"uniformTypeIdentifier"] isEqualToString:(__bridge NSString *)kUTTypeGIF]) { _assetSubType = QMUIAssetSubTypeGIF; } else { - if (@available(iOS 9.1, *)) { - if (phAsset.mediaSubtypes & PHAssetMediaSubtypePhotoLive) { - _assetSubType = QMUIAssetSubTypeLivePhoto; - } else { - _assetSubType = QMUIAssetSubTypeImage; - } + if (phAsset.mediaSubtypes & PHAssetMediaSubtypePhotoLive) { + _assetSubType = QMUIAssetSubTypeLivePhoto; } else { _assetSubType = QMUIAssetSubTypeImage; } diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.h b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.h index c0d1ddf0..d4e236b6 100644 --- a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.h +++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIAssetsGroup.h diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.m b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.m index 999c42c6..528916b4 100644 --- a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.m +++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIAssetsGroup.m @@ -30,8 +30,7 @@ @implementation QMUIAssetsGroup - (instancetype)initWithPHCollection:(PHAssetCollection *)phAssetCollection fetchAssetsOptions:(PHFetchOptions *)pHFetchOptions { self = [super init]; if (self) { - PHFetchResult *phFetchResult = [PHAsset fetchAssetsInAssetCollection:phAssetCollection options:pHFetchOptions]; - self.phFetchResult = phFetchResult; + self.phFetchResult = [PHAsset fetchAssetsInAssetCollection:phAssetCollection options:pHFetchOptions]; self.phAssetCollection = phAssetCollection; } return self; diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.h b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.h index 95e8b489..178da093 100644 --- a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.h +++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIAssetsManager.h @@ -122,7 +122,7 @@ extern void QMUISaveVideoAtPathToSavedPhotosAlbumWithAlbumAssetsGroup(NSString * * * @return 返回包含所有合适相册的数组 */ -+ (NSArray *)fetchAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbum:(BOOL)showSmartAlbum; ++ (NSArray *)fetchAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbum:(BOOL)showSmartAlbum; /// 获取一个 PHAssetCollection 中创建日期最新的资源 + (PHAsset *)fetchLatestAssetWithAssetCollection:(PHAssetCollection *)assetCollection; diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.m b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.m index f9f7c4be..ff0cf089 100644 --- a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.m +++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIAssetsManager.m @@ -91,14 +91,14 @@ + (void)requestAuthorization:(void(^)(QMUIAssetAuthorizationStatus status))handl - (void)enumerateAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbumIfSupported:(BOOL)showSmartAlbumIfSupported usingBlock:(void (^)(QMUIAssetsGroup *resultAssetsGroup))enumerationBlock { // 根据条件获取所有合适的相册,并保存到临时数组中 - NSArray *tempAlbumsArray = [PHPhotoLibrary fetchAllAlbumsWithAlbumContentType:contentType showEmptyAlbum:showEmptyAlbum showSmartAlbum:showSmartAlbumIfSupported]; + NSArray *tempAlbumsArray = [PHPhotoLibrary fetchAllAlbumsWithAlbumContentType:contentType showEmptyAlbum:showEmptyAlbum showSmartAlbum:showSmartAlbumIfSupported]; // 创建一个 PHFetchOptions,用于 QMUIAssetsGroup 对资源的排序以及对内容类型进行控制 PHFetchOptions *phFetchOptions = [PHPhotoLibrary createFetchOptionsWithAlbumContentType:contentType]; // 遍历结果,生成对应的 QMUIAssetsGroup,并调用 enumerationBlock - for (NSUInteger i = 0; i < [tempAlbumsArray count]; i++) { - PHAssetCollection *phAssetCollection = [tempAlbumsArray objectAtIndex:i]; + for (NSUInteger i = 0; i < tempAlbumsArray.count; i++) { + PHAssetCollection *phAssetCollection = tempAlbumsArray[i]; QMUIAssetsGroup *assetsGroup = [[QMUIAssetsGroup alloc] initWithPHCollection:phAssetCollection fetchAssetsOptions:phFetchOptions]; if (enumerationBlock) { enumerationBlock(assetsGroup); @@ -212,8 +212,8 @@ + (PHFetchOptions *)createFetchOptionsWithAlbumContentType:(QMUIAlbumContentType return fetchOptions; } -+ (NSArray *)fetchAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbum:(BOOL)showSmartAlbum { - NSMutableArray *tempAlbumsArray = [[NSMutableArray alloc] init]; ++ (NSArray *)fetchAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbum:(BOOL)showSmartAlbum { + NSMutableArray *tempAlbumsArray = [[NSMutableArray alloc] init]; // 创建一个 PHFetchOptions,用于创建 QMUIAssetsGroup 对资源的排序和类型进行控制 PHFetchOptions *fetchOptions = [PHPhotoLibrary createFetchOptionsWithAlbumContentType:contentType]; @@ -284,7 +284,7 @@ + (NSArray *)fetchAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentTyp } } - NSArray *resultAlbumsArray = [tempAlbumsArray copy]; + NSArray *resultAlbumsArray = [tempAlbumsArray copy]; return resultAlbumsArray; } diff --git a/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.h b/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.h index f331845e..ec7f73fe 100644 --- a/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.h +++ b/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // CAAnimation+QMUI.h @@ -15,6 +15,8 @@ #import +// 这个文件依赖了 QMUIMultipleDelegates,无法作为 UIKitExtensions 的一部分,所以放在 QMUIComponents 内 + @interface CAAnimation (QMUI) @property(nonatomic, copy) void (^qmui_animationDidStartBlock)(__kindof CAAnimation *aAnimation); diff --git a/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.m b/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.m index 6bf6cfc2..9a1e5831 100644 --- a/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.m +++ b/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // CAAnimation+QMUI.m diff --git a/QMUI/QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.h b/QMUI/QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.h new file mode 100644 index 00000000..09c2773d --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.h @@ -0,0 +1,34 @@ +/** + * 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. + */ +// +// CALayer+QMUIViewAnimation.h +// QMUIKit +// +// Created by ziezheng on 2020/4/4. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface CALayer (QMUIViewAnimation) + +/** + 开启了该属性的 CALayer 可在 +[UIView animateWithDuration:animations:] 执行动画,系统默认是不支持这种做法的。 + + @code + [UIView animateWithDuration:1 animations:^{ + layer.frame = xxx; + } completion:nil]; + @endcode + */ +@property(nonatomic, assign) BOOL qmui_viewAnimationEnabled; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.m b/QMUI/QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.m new file mode 100644 index 00000000..0c1dcea1 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.m @@ -0,0 +1,98 @@ +/** + * 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. + */ +// +// CALayer+QMUIViewAnimation.m +// QMUIKit +// +// Created by ziezheng on 2020/4/4. +// + +#import "CALayer+QMUIViewAnimation.h" +#import "CALayer+QMUI.h" +#import "QMUICore.h" +#import "QMUIMultipleDelegates.h" + + +@interface _QMUICALayerDelegator : NSObject + +@end + +@implementation _QMUICALayerDelegator + ++ (instancetype)sharedDelegator { + static dispatch_once_t onceToken; + static _QMUICALayerDelegator *instance = nil; + dispatch_once(&onceToken,^{ + instance = [[super allocWithZone:NULL] init]; + }); + return instance; +} + ++ (id)allocWithZone:(struct _NSZone *)zone { + return [self sharedDelegator]; +} + +- (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event { + static UIView *standardView = nil; + if (!standardView) standardView = UIView.new; + // 被 +[UIView animateWithDuration:animations:] 包裹的代码可利用任意 UIView 的 actionForLayer:forKey: 来获得默认的 CAAction + id action = [standardView actionForLayer:standardView.layer forKey:event]; + if (action == [NSNull null]) { + // -[CALayer actionForKey:] 会先询问本代理,一旦代理返回了 NSNull, 则不会执行 self.actions 里隐式动画,为保持 CALayer 的原有逻辑,这里返回 nil,详见 -[CALayer actionForKey:] 的文档描述。 + return nil; + } else { + return action; + } +} + +@end + +@implementation CALayer (QMUIViewAnimation) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation([CALayer class], @selector(addAnimation:forKey:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(CALayer *selfObject, CAAnimation *animation, NSString *key) { + if (selfObject.qmui_viewAnimationEnabled) { + BOOL isViewAnimtion = [animation isKindOfClass:CABasicAnimation.class] && [animation.delegate isKindOfClass:NSClassFromString(@"UIViewAnimationState")]; + if (isViewAnimtion) { + // 这里需要清空 fromValue 和 toValue,后面会在 CAMediaTimingCopyRenderTiming 取到这个 animtion 的参数并设置到 CATransaction 中,让 Layer 改变属性时,运用上这些动画 + ((CABasicAnimation *)animation).fromValue = nil; + ((CABasicAnimation *)animation).toValue = nil; + // 这个机制下的 toValue 已是最终值,这里 additive 要设置成 NO,否则会多叠加一次计算结果,导致动画出错。 + ((CABasicAnimation *)animation).additive = NO; + } + } + void (*originSelectorIMP)(id, SEL, CAAnimation *, NSString *); + originSelectorIMP = (void (*)(id, SEL, CAAnimation *, NSString *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, animation, key); + + }; + }); + }); +} + + +static char kAssociatedObjectKey_qmuiviewAnimationEnabled; +- (void)setQmui_viewAnimationEnabled:(BOOL)qmui_viewAnimationEnabled { + QMUIAssert(!self.qmui_isRootLayerOfView, @"CALayer (QMUIViewAnimation)", @"UIView 本身的 Layer 无须开启 %s", __func__); + objc_setAssociatedObject(self, &kAssociatedObjectKey_qmuiviewAnimationEnabled, @(qmui_viewAnimationEnabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_viewAnimationEnabled) { + self.qmui_multipleDelegatesEnabled = YES; + self.delegate = [_QMUICALayerDelegator sharedDelegator]; + } else { + [self qmui_removeDelegate:[_QMUICALayerDelegator sharedDelegator]]; + } +} + +- (BOOL)qmui_viewAnimationEnabled { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_qmuiviewAnimationEnabled)) boolValue]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.h b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.h index ad06810e..aef27cd3 100644 --- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.h +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIAlbumViewController.h diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.m index 4124fc9e..201b7c08 100644 --- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.m +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIAlbumViewController.m @@ -20,6 +20,7 @@ #import "QMUIAssetsManager.h" #import "QMUIImagePickerViewController.h" #import "QMUIImagePickerHelper.h" +#import "QMUIAppearance.h" #import #import #import @@ -45,13 +46,8 @@ + (void)initialize { - (void)didInitializeWithStyle:(UITableViewCellStyle)style { [super didInitializeWithStyle:style]; - self.albumImageSize = [QMUIAlbumTableViewCell appearance].albumImageSize; - self.albumImageMarginLeft = [QMUIAlbumTableViewCell appearance].albumImageMarginLeft; - self.albumNameInsets = [QMUIAlbumTableViewCell appearance].albumNameInsets; - self.albumNameFont = [QMUIAlbumTableViewCell appearance].albumNameFont; - self.albumNameColor = [QMUIAlbumTableViewCell appearance].albumNameColor; - self.albumAssetsNumberFont = [QMUIAlbumTableViewCell appearance].albumAssetsNumberFont; - self.albumAssetsNumberColor = [QMUIAlbumTableViewCell appearance].albumAssetsNumberColor; + + [self qmui_applyAppearance]; self.imageView.contentMode = UIViewContentModeScaleAspectFill; self.imageView.clipsToBounds = YES; @@ -109,23 +105,19 @@ - (void)setAlbumAssetsNumberColor:(UIColor *)albumAssetsNumberColor { @implementation QMUIAlbumViewController (UIAppearance) ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; +} + + (void)initialize { - static dispatch_once_t onceToken1; - dispatch_once(&onceToken1, ^{ - [self appearance]; // +initialize 时就先设置好默认样式 + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self initAppearance]; }); } -static QMUIAlbumViewController *albumViewControllerAppearance; -+ (nonnull instancetype)appearance { - static dispatch_once_t onceToken2; - dispatch_once(&onceToken2, ^{ - if (!albumViewControllerAppearance) { - albumViewControllerAppearance = [[QMUIAlbumViewController alloc] init]; - albumViewControllerAppearance.albumTableViewCellHeight = 88; - } - }); - return albumViewControllerAppearance; ++ (void)initAppearance { + QMUIAlbumViewController.appearance.albumTableViewCellHeight = 88; } @end @@ -144,10 +136,7 @@ @implementation QMUIAlbumViewController - (void)didInitialize { [super didInitialize]; _shouldShowDefaultLoadingView = YES; - if (albumViewControllerAppearance) { - // 避免 albumViewControllerAppearance init 时走到这里来,导致死循环 - self.albumTableViewCellHeight = [QMUIAlbumViewController appearance].albumTableViewCellHeight; - } + [self qmui_applyAppearance]; } - (void)setupNavigationItems { @@ -186,23 +175,37 @@ - (void)viewDidLoad { if (self.shouldShowDefaultLoadingView) { [self showEmptyViewWithLoading]; } - dispatch_async(dispatch_get_global_queue(0, 0), ^{ - __weak __typeof(self)weakSelf = self; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [[QMUIAssetsManager sharedInstance] enumerateAllAlbumsWithAlbumContentType:self.contentType usingBlock:^(QMUIAssetsGroup *resultAssetsGroup) { - dispatch_async(dispatch_get_main_queue(), ^{ - // 这里需要对 UI 进行操作,因此放回主线程处理 - __strong __typeof(weakSelf)strongSelf = weakSelf; - if (resultAssetsGroup) { - [strongSelf.albumsArray addObject:resultAssetsGroup]; - } else { - [strongSelf refreshAlbumAndShowEmptyTipIfNeed]; - } - }); + if (resultAssetsGroup) { + [self.albumsArray addObject:resultAssetsGroup]; + } else { + // 意味着遍历完所有的相簿了 + [self sortAlbumArray]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self refreshAlbumAndShowEmptyTipIfNeed]; + }); + } }]; }); } } +- (void)sortAlbumArray { + // 把隐藏相册排序强制放到最后 + __block QMUIAssetsGroup *hiddenGroup = nil; + [self.albumsArray enumerateObjectsUsingBlock:^(QMUIAssetsGroup * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (obj.phAssetCollection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumAllHidden) { + hiddenGroup = obj; + *stop = YES; + } + }]; + if (hiddenGroup) { + [self.albumsArray removeObject:hiddenGroup]; + [self.albumsArray addObject:hiddenGroup]; + } +} + - (void)refreshAlbumAndShowEmptyTipIfNeed { if ([self.albumsArray count] > 0) { if ([self.albumViewControllerDelegate respondsToSelector:@selector(albumViewControllerWillFinishLoading:)]) { @@ -224,7 +227,7 @@ - (void)pickAlbumsGroup:(QMUIAssetsGroup *)assetsGroup animated:(BOOL)animated { if (!self.imagePickerViewController) { self.imagePickerViewController = [self.albumViewControllerDelegate imagePickerViewControllerForAlbumViewController:self]; } - NSAssert(self.imagePickerViewController, @"self.%@ 必须实现 %@ 并返回一个 %@ 对象", NSStringFromSelector(@selector(albumViewControllerDelegate)), NSStringFromSelector(@selector(imagePickerViewControllerForAlbumViewController:)), NSStringFromClass([QMUIImagePickerViewController class])); + QMUIAssert(!!self.imagePickerViewController, NSStringFromClass(self.class), NSStringFromClass(self.class), @"self.%@ 必须实现 %@ 并返回一个 %@ 对象", NSStringFromSelector(@selector(albumViewControllerDelegate)), NSStringFromSelector(@selector(imagePickerViewControllerForAlbumViewController:)), NSStringFromClass([QMUIImagePickerViewController class])); [self.imagePickerViewController refreshWithAssetsGroup:assetsGroup]; self.imagePickerViewController.title = [assetsGroup name]; @@ -253,12 +256,9 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N cell = [[QMUIAlbumTableViewCell alloc] initForTableView:tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:kCellIdentifer]; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; } - QMUIAssetsGroup *assetsGroup = [self.albumsArray objectAtIndex:indexPath.row]; - // 显示相册缩略图 + QMUIAssetsGroup *assetsGroup = self.albumsArray[indexPath.row]; cell.imageView.image = [assetsGroup posterImageWithSize:CGSizeMake(self.albumTableViewCellHeight, self.albumTableViewCellHeight)]; - // 显示相册名称 cell.textLabel.text = [assetsGroup name]; - // 显示相册中所包含的资源数量 cell.detailTextLabel.text = [NSString stringWithFormat:@"· %@", @(assetsGroup.numberOfAssets)]; [cell updateCellAppearanceWithIndexPath:indexPath]; return cell; diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h index 79f576c8..55a1e61b 100644 --- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIImagePickerCollectionViewCell.h diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m index 698e3075..44404cd9 100644 --- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIImagePickerCollectionViewCell.m @@ -23,6 +23,7 @@ #import "QMUIButton.h" #import "UIView+QMUI.h" #import "NSString+QMUI.h" +#import "QMUIAppearance.h" @interface QMUIImagePickerCollectionViewCell () @@ -55,14 +56,7 @@ - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self initImagePickerCollectionViewCellUI]; - self.favoriteImage = [QMUIImagePickerCollectionViewCell appearance].favoriteImage; - self.favoriteImageMargins = [QMUIImagePickerCollectionViewCell appearance].favoriteImageMargins; - self.checkboxImage = [QMUIImagePickerCollectionViewCell appearance].checkboxImage; - self.checkboxCheckedImage = [QMUIImagePickerCollectionViewCell appearance].checkboxCheckedImage; - self.checkboxButtonMargins = [QMUIImagePickerCollectionViewCell appearance].checkboxButtonMargins; - self.videoDurationLabelFont = [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelFont; - self.videoDurationLabelTextColor = [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelTextColor; - self.videoDurationLabelMargins = [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelMargins; + [self qmui_applyAppearance]; } return self; } diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h index 2247b1d5..8ee0bfa1 100644 --- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIImagePickerHelper.h diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m index e44017b0..f932d96d 100644 --- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIImagePickerHelper.m diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h index a3995b38..b9315298 100644 --- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIImagePickerPreviewViewController.h diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m index 5b15b3d7..68866778 100644 --- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIImagePickerPreviewViewController.m @@ -27,31 +27,27 @@ #import "QMUIAlertController.h" #import "UIImage+QMUI.h" #import "UIView+QMUI.h" -#import "UIControl+QMUI.h" #import "QMUILog.h" +#import "QMUIAppearance.h" #pragma mark - QMUIImagePickerPreviewViewController (UIAppearance) @implementation QMUIImagePickerPreviewViewController (UIAppearance) ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; +} + + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - [self appearance]; // +initialize 时就先设置好默认样式 + [self initAppearance]; }); } -static QMUIImagePickerPreviewViewController *imagePickerPreviewViewControllerAppearance; -+ (nonnull instancetype)appearance { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - if (!imagePickerPreviewViewControllerAppearance) { - imagePickerPreviewViewControllerAppearance = [[QMUIImagePickerPreviewViewController alloc] init]; - imagePickerPreviewViewControllerAppearance.toolBarBackgroundColor = UIColorMakeWithRGBA(27, 27, 27, .9f); - imagePickerPreviewViewControllerAppearance.toolBarTintColor = UIColorWhite; - } - }); - return imagePickerPreviewViewControllerAppearance; ++ (void)initAppearance { + QMUIImagePickerPreviewViewController.appearance.toolBarBackgroundColor = UIColorMakeWithRGBA(27, 27, 27, .9f); + QMUIImagePickerPreviewViewController.appearance.toolBarTintColor = UIColorWhite; } @end @@ -64,11 +60,8 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { self.maximumSelectImageCount = INT_MAX; self.minimumSelectImageCount = 0; - if (imagePickerPreviewViewControllerAppearance) { - // 避免 imagePickerPreviewViewControllerAppearance init 时走到这里来,导致死循环 - self.toolBarBackgroundColor = [QMUIImagePickerPreviewViewController appearance].toolBarBackgroundColor; - self.toolBarTintColor = [QMUIImagePickerPreviewViewController appearance].toolBarTintColor; - } + + [self qmui_applyAppearance]; } return self; } @@ -109,10 +102,6 @@ - (void)viewWillAppear:(BOOL)animated { self.checkboxButton.selected = [self.selectedImageAssetArray containsObject:imageAsset]; } - BeginIgnoreDeprecatedWarning - [[UIApplication sharedApplication] setStatusBarHidden:YES]; - EndIgnoreDeprecatedWarning - if ([self conformsToProtocol:@protocol(QMUICustomNavigationBarTransitionDelegate)]) { UIViewController *vc = (UIViewController *)self; if ([vc respondsToSelector:@selector(shouldCustomizeNavigationBarTransitionIfHideable)] && @@ -126,10 +115,6 @@ - (void)viewWillAppear:(BOOL)animated { - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; - BeginIgnoreDeprecatedWarning - [[UIApplication sharedApplication] setStatusBarHidden:NO]; - EndIgnoreDeprecatedWarning - if ([self conformsToProtocol:@protocol(QMUICustomNavigationBarTransitionDelegate)]) { UIViewController *vc = (UIViewController *)self; if ([vc respondsToSelector:@selector(shouldCustomizeNavigationBarTransitionIfHideable)] && @@ -145,9 +130,9 @@ - (void)viewDidLayoutSubviews { self.topToolBarView.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), NavigationContentTopConstant); CGFloat topToolbarPaddingTop = SafeAreaInsetsConstantForDeviceWithNotch.top; CGFloat topToolbarContentHeight = CGRectGetHeight(self.topToolBarView.bounds) - topToolbarPaddingTop; - self.backButton.frame = CGRectSetXY(self.backButton.frame, 16 + self.view.qmui_safeAreaInsets.left, topToolbarPaddingTop + CGFloatGetCenter(topToolbarContentHeight, CGRectGetHeight(self.backButton.frame))); + self.backButton.frame = CGRectSetXY(self.backButton.frame, 16 + self.view.safeAreaInsets.left, topToolbarPaddingTop + CGFloatGetCenter(topToolbarContentHeight, CGRectGetHeight(self.backButton.frame))); if (!self.checkboxButton.hidden) { - self.checkboxButton.frame = CGRectSetXY(self.checkboxButton.frame, CGRectGetWidth(self.topToolBarView.frame) - 10 - self.view.qmui_safeAreaInsets.right - CGRectGetWidth(self.checkboxButton.frame), topToolbarPaddingTop + CGFloatGetCenter(topToolbarContentHeight, CGRectGetHeight(self.checkboxButton.frame))); + self.checkboxButton.frame = CGRectSetXY(self.checkboxButton.frame, CGRectGetWidth(self.topToolBarView.frame) - 10 - self.view.safeAreaInsets.right - CGRectGetWidth(self.checkboxButton.frame), topToolbarPaddingTop + CGFloatGetCenter(topToolbarContentHeight, CGRectGetHeight(self.checkboxButton.frame))); } } @@ -198,10 +183,8 @@ - (NSUInteger)numberOfImagesInImagePreviewView:(QMUIImagePreviewView *)imagePrev - (QMUIImagePreviewMediaType)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView assetTypeAtIndex:(NSUInteger)index { QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:index]; if (imageAsset.assetType == QMUIAssetTypeImage) { - if (@available(iOS 9.1, *)) { - if (imageAsset.assetSubType == QMUIAssetSubTypeLivePhoto) { - return QMUIImagePreviewMediaTypeLivePhoto; - } + if (imageAsset.assetSubType == QMUIAssetSubTypeLivePhoto) { + return QMUIImagePreviewMediaTypeLivePhoto; } return QMUIImagePreviewMediaTypeImage; } else if (imageAsset.assetType == QMUIAssetTypeVideo) { @@ -355,13 +338,8 @@ - (void)requestImageForZoomImageView:(QMUIZoomImageView *)zoomImageView withInde // 这么写是为了消除 Xcode 的 API available warning BOOL isLivePhoto = NO; - if (@available(iOS 9.1, *)) { - if (imageAsset.assetSubType == QMUIAssetSubTypeLivePhoto) { - isLivePhoto = YES; - } - } - - if (isLivePhoto) { + if (imageAsset.assetSubType == QMUIAssetSubTypeLivePhoto) { + isLivePhoto = YES; imageView.tag = -1; imageAsset.requestID = [imageAsset requestLivePhotoWithCompletion:^void(PHLivePhoto *livePhoto, NSDictionary *info) { // 这里可能因为 imageView 复用,导致前面的请求得到的结果显示到别的 imageView 上, @@ -373,9 +351,7 @@ - (void)requestImageForZoomImageView:(QMUIZoomImageView *)zoomImageView withInde if (!loadICloudImageFault && (isNewRequest || isCurrentRequest)) { // 如果是走 PhotoKit 的逻辑,那么这个 block 会被多次调用,并且第一次调用时返回的图片是一张小图, // 这时需要把图片放大到跟屏幕一样大,避免后面加载大图后图片的显示会有跳动 - if (@available(iOS 9.1, *)) { - imageView.livePhoto = livePhoto; - } + imageView.livePhoto = livePhoto; } BOOL downloadSucceed = (livePhoto && !info) || (![[info objectForKey:PHLivePhotoInfoCancelledKey] boolValue] && ![info objectForKey:PHLivePhotoInfoErrorKey] && ![[info objectForKey:PHLivePhotoInfoIsDegradedKey] boolValue]); if (downloadSucceed) { @@ -392,6 +368,9 @@ - (void)requestImageForZoomImageView:(QMUIZoomImageView *)zoomImageView withInde }); } withProgressHandler:phProgressHandler]; imageView.tag = imageAsset.requestID; + } + + if (isLivePhoto) { } else if (imageAsset.assetSubType == QMUIAssetSubTypeGIF) { [imageAsset requestImageData:^(NSData *imageData, NSDictionary *info, BOOL isGIF, BOOL isHEIC) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h index 1cc2a1ff..bd0082b7 100644 --- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIImagePickerViewController.h diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m index 04796fa2..d1bd78c5 100644 --- a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIImagePickerViewController.m @@ -28,9 +28,9 @@ #import "UIView+QMUI.h" #import #import "QMUIEmptyView.h" -#import "UIControl+QMUI.h" #import "UIViewController+QMUI.h" #import "QMUILog.h" +#import "QMUIAppearance.h" static NSString * const kVideoCellIdentifier = @"video"; static NSString * const kImageOrUnknownCellIdentifier = @"imageorunknown"; @@ -40,23 +40,19 @@ @implementation QMUIImagePickerViewController (UIAppearance) ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; +} + + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - [self appearance]; // +initialize 时就先设置好默认样式 + [self initAppearance]; }); } -static QMUIImagePickerViewController *imagePickerViewControllerAppearance; -+ (nonnull instancetype)appearance { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - if (!imagePickerViewControllerAppearance) { - imagePickerViewControllerAppearance = [[QMUIImagePickerViewController alloc] init]; - imagePickerViewControllerAppearance.minimumImageWidth = 75; - } - }); - return imagePickerViewControllerAppearance; ++ (void)initAppearance { + QMUIImagePickerViewController.appearance.minimumImageWidth = 75; } @end @@ -75,96 +71,29 @@ @implementation QMUIImagePickerViewController - (void)didInitialize { [super didInitialize]; - if (imagePickerViewControllerAppearance) { - // 避免 imagePickerViewControllerAppearance init 时走到这里来,导致死循环 - self.minimumImageWidth = [QMUIImagePickerViewController appearance].minimumImageWidth; - } + + [self qmui_applyAppearance]; + _allowsMultipleSelection = YES; _maximumSelectImageCount = INT_MAX; _minimumSelectImageCount = 0; _shouldShowDefaultLoadingView = YES; - // 为了让使用者可以在 init 完就可以直接改 UI 相关的 property,这里提前触发 loadView - [self loadViewIfNeeded]; } - (void)dealloc { - self.collectionView.dataSource = nil; - self.collectionView.delegate = nil; + _collectionView.dataSource = nil; + _collectionView.delegate = nil; } -- (void)initSubviews { - [super initSubviews]; - - _collectionViewLayout = [[UICollectionViewFlowLayout alloc] init]; - CGFloat inset = PixelOne * 2; // no why, just beautiful - self.collectionViewLayout.sectionInset = UIEdgeInsetsMake(inset, inset, inset, inset); - self.collectionViewLayout.minimumLineSpacing = self.collectionViewLayout.sectionInset.bottom; - self.collectionViewLayout.minimumInteritemSpacing = self.collectionViewLayout.sectionInset.left; - - _collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.collectionViewLayout]; - self.collectionView.delegate = self; - self.collectionView.dataSource = self; - self.collectionView.showsHorizontalScrollIndicator = NO; - self.collectionView.alwaysBounceHorizontal = NO; - self.collectionView.backgroundColor = UIColorClear; - [self.collectionView registerClass:[QMUIImagePickerCollectionViewCell class] forCellWithReuseIdentifier:kVideoCellIdentifier]; - [self.collectionView registerClass:[QMUIImagePickerCollectionViewCell class] forCellWithReuseIdentifier:kImageOrUnknownCellIdentifier]; - if (@available(iOS 11, *)) { - self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } else { - self.automaticallyAdjustsScrollViewInsets = NO; - } +- (void)viewDidLoad { + [super viewDidLoad]; + self.view.backgroundColor = UIColorWhite; [self.view addSubview:self.collectionView]; - - // 只有允许多选时,才显示底部工具 if (self.allowsMultipleSelection) { - - _operationToolBarView = [[UIView alloc] init]; - self.operationToolBarView.backgroundColor = UIColorWhite; - self.operationToolBarView.qmui_borderPosition = QMUIViewBorderPositionTop; [self.view addSubview:self.operationToolBarView]; - - _sendButton = [[QMUIButton alloc] init]; - self.sendButton.enabled = NO; - self.sendButton.titleLabel.font = UIFontMake(16); - self.sendButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight; - [self.sendButton setTitleColor:UIColorMake(124, 124, 124) forState:UIControlStateNormal]; - [self.sendButton setTitleColor:UIColorGray forState:UIControlStateDisabled]; - [self.sendButton setTitle:@"发送" forState:UIControlStateNormal]; - self.sendButton.qmui_outsideEdge = UIEdgeInsetsMake(-12, -20, -12, -20); - [self.sendButton sizeToFit]; - [self.sendButton addTarget:self action:@selector(handleSendButtonClick:) forControlEvents:UIControlEventTouchUpInside]; - [self.operationToolBarView addSubview:self.sendButton]; - - _previewButton = [[QMUIButton alloc] init]; - self.previewButton.enabled = NO; - self.previewButton.titleLabel.font = self.sendButton.titleLabel.font; - [self.previewButton setTitleColor:[self.sendButton titleColorForState:UIControlStateNormal] forState:UIControlStateNormal]; - [self.previewButton setTitleColor:[self.sendButton titleColorForState:UIControlStateDisabled] forState:UIControlStateDisabled]; - [self.previewButton setTitle:@"预览" forState:UIControlStateNormal]; - self.previewButton.qmui_outsideEdge = UIEdgeInsetsMake(-12, -20, -12, -20); - [self.previewButton sizeToFit]; - [self.previewButton addTarget:self action:@selector(handlePreviewButtonClick:) forControlEvents:UIControlEventTouchUpInside]; - [self.operationToolBarView addSubview:self.previewButton]; - - _imageCountLabel = [[UILabel alloc] init]; - self.imageCountLabel.userInteractionEnabled = NO;// 不要影响 sendButton 的事件 - self.imageCountLabel.backgroundColor = ButtonTintColor; - self.imageCountLabel.textColor = UIColorWhite; - self.imageCountLabel.font = UIFontMake(12); - self.imageCountLabel.textAlignment = NSTextAlignmentCenter; - self.imageCountLabel.lineBreakMode = NSLineBreakByCharWrapping; - self.imageCountLabel.layer.masksToBounds = YES; - self.imageCountLabel.hidden = YES; - [self.operationToolBarView addSubview:self.imageCountLabel]; } } -- (void)viewDidLoad { - [super viewDidLoad]; - self.view.backgroundColor = UIColorWhite; -} - - (void)setupNavigationItems { [super setupNavigationItems]; self.navigationItem.rightBarButtonItem = [UIBarButtonItem qmui_itemWithTitle:@"取消" target:self action:@selector(handleCancelPickerImage:)]; @@ -193,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 先滚到列表顶部然后跳到列表底部) @@ -217,7 +156,7 @@ - (void)viewDidLayoutSubviews { if (!CGSizeEqualToSize(self.collectionView.frame.size, self.view.bounds.size)) { self.collectionView.frame = self.view.bounds; } - UIEdgeInsets contentInset = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator, self.collectionView.qmui_safeAreaInsets.left, MAX(operationToolBarViewHeight, self.collectionView.qmui_safeAreaInsets.bottom), self.collectionView.qmui_safeAreaInsets.right); + UIEdgeInsets contentInset = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator, self.collectionView.safeAreaInsets.left, MAX(operationToolBarViewHeight, self.collectionView.safeAreaInsets.bottom), self.collectionView.safeAreaInsets.right); if (!UIEdgeInsetsEqualToEdgeInsets(self.collectionView.contentInset, contentInset)) { self.collectionView.contentInset = contentInset; self.collectionView.scrollIndicatorInsets = UIEdgeInsetsMake(contentInset.top, 0, contentInset.bottom, 0); @@ -249,7 +188,7 @@ - (void)refreshWithAssetsGroup:(QMUIAssetsGroup *)assetsGroup { if (self.shouldShowDefaultLoadingView) { [self showEmptyViewWithLoading]; } - dispatch_async(dispatch_get_global_queue(0, 0), ^{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [assetsGroup enumerateAssetsWithOptions:albumSortType usingBlock:^(QMUIAsset *resultAsset) { // 这里需要对 UI 进行操作,因此放回主线程处理 dispatch_async(dispatch_get_main_queue(), ^{ @@ -305,18 +244,118 @@ - (void)setMinimumImageWidth:(CGFloat)minimumImageWidth { } - (void)scrollToInitialPositionIfNeeded { - if (self.collectionView.qmui_visible && self.isImagesAssetLoaded && !self.hasScrollToInitialPosition) { + if (_collectionView.qmui_visible && self.isImagesAssetLoaded && !self.hasScrollToInitialPosition) { if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(albumSortTypeForImagePickerViewController:)] && [self.imagePickerViewControllerDelegate albumSortTypeForImagePickerViewController:self] == QMUIAlbumSortTypeReverse) { - [self.collectionView qmui_scrollToTop]; + [_collectionView qmui_scrollToTop]; } else { - [self.collectionView qmui_scrollToBottom]; + [_collectionView qmui_scrollToBottom]; } self.hasScrollToInitialPosition = YES; } } -- (void)willPopInNavigationControllerWithAnimated:(BOOL)animated { - self.hasScrollToInitialPosition = NO; +#pragma mark - Getters & Setters + +@synthesize collectionViewLayout = _collectionViewLayout; +- (UICollectionViewFlowLayout *)collectionViewLayout { + if (!_collectionViewLayout) { + _collectionViewLayout = [[UICollectionViewFlowLayout alloc] init]; + CGFloat inset = PixelOne * 2; // no why, just beautiful + _collectionViewLayout.sectionInset = UIEdgeInsetsMake(inset, inset, inset, inset); + _collectionViewLayout.minimumLineSpacing = _collectionViewLayout.sectionInset.bottom; + _collectionViewLayout.minimumInteritemSpacing = _collectionViewLayout.sectionInset.left; + } + return _collectionViewLayout; +} + +@synthesize collectionView = _collectionView; +- (UICollectionView *)collectionView { + if (!_collectionView) { + _collectionView = [[UICollectionView alloc] initWithFrame:self.isViewLoaded ? self.view.bounds : CGRectZero collectionViewLayout:self.collectionViewLayout]; + _collectionView.delegate = self; + _collectionView.dataSource = self; + _collectionView.showsHorizontalScrollIndicator = NO; + _collectionView.alwaysBounceHorizontal = NO; + _collectionView.backgroundColor = UIColorClear; + [_collectionView registerClass:[QMUIImagePickerCollectionViewCell class] forCellWithReuseIdentifier:kVideoCellIdentifier]; + [_collectionView registerClass:[QMUIImagePickerCollectionViewCell class] forCellWithReuseIdentifier:kImageOrUnknownCellIdentifier]; + _collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } + return _collectionView; +} + +@synthesize operationToolBarView = _operationToolBarView; +- (UIView *)operationToolBarView { + if (!_operationToolBarView) { + _operationToolBarView = [[UIView alloc] init]; + _operationToolBarView.backgroundColor = UIColorWhite; + _operationToolBarView.qmui_borderPosition = QMUIViewBorderPositionTop; + + [_operationToolBarView addSubview:self.sendButton]; + [_operationToolBarView addSubview:self.previewButton]; + [_operationToolBarView addSubview:self.imageCountLabel]; + } + return _operationToolBarView; +} + +@synthesize sendButton = _sendButton; +- (QMUIButton *)sendButton { + if (!_sendButton) { + _sendButton = [[QMUIButton alloc] init]; + _sendButton.enabled = NO; + _sendButton.titleLabel.font = UIFontMake(16); + _sendButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight; + [_sendButton setTitleColor:UIColorMake(124, 124, 124) forState:UIControlStateNormal]; + [_sendButton setTitleColor:UIColorGray forState:UIControlStateDisabled]; + [_sendButton setTitle:@"发送" forState:UIControlStateNormal]; + _sendButton.qmui_outsideEdge = UIEdgeInsetsMake(-12, -20, -12, -20); + [_sendButton sizeToFit]; + [_sendButton addTarget:self action:@selector(handleSendButtonClick:) forControlEvents:UIControlEventTouchUpInside]; + } + return _sendButton; +} + +@synthesize previewButton = _previewButton; +- (QMUIButton *)previewButton { + if (!_previewButton) { + _previewButton = [[QMUIButton alloc] init]; + _previewButton.enabled = NO; + _previewButton.titleLabel.font = self.sendButton.titleLabel.font; + [_previewButton setTitleColor:[self.sendButton titleColorForState:UIControlStateNormal] forState:UIControlStateNormal]; + [_previewButton setTitleColor:[self.sendButton titleColorForState:UIControlStateDisabled] forState:UIControlStateDisabled]; + [_previewButton setTitle:@"预览" forState:UIControlStateNormal]; + _previewButton.qmui_outsideEdge = UIEdgeInsetsMake(-12, -20, -12, -20); + [_previewButton sizeToFit]; + [_previewButton addTarget:self action:@selector(handlePreviewButtonClick:) forControlEvents:UIControlEventTouchUpInside]; + } + return _previewButton; +} + +@synthesize imageCountLabel = _imageCountLabel; +- (UILabel *)imageCountLabel { + if (!_imageCountLabel) { + _imageCountLabel = [[UILabel alloc] init]; + _imageCountLabel.userInteractionEnabled = NO;// 不要影响 sendButton 的事件 + _imageCountLabel.backgroundColor = ButtonTintColor; + _imageCountLabel.textColor = UIColorWhite; + _imageCountLabel.font = UIFontMake(12); + _imageCountLabel.textAlignment = NSTextAlignmentCenter; + _imageCountLabel.lineBreakMode = NSLineBreakByCharWrapping; + _imageCountLabel.layer.masksToBounds = YES; + _imageCountLabel.hidden = YES; + } + return _imageCountLabel; +} + +- (void)setAllowsMultipleSelection:(BOOL)allowsMultipleSelection { + _allowsMultipleSelection = allowsMultipleSelection; + if (self.isViewLoaded) { + if (_allowsMultipleSelection) { + [self.view addSubview:self.operationToolBarView]; + } else { + [_operationToolBarView removeFromSuperview]; + } + } } #pragma mark - @@ -408,13 +447,13 @@ - (void)handleCancelPickerImage:(id)sender { } - (void)handleCheckBoxButtonClick:(UIButton *)checkboxButton { - NSIndexPath *indexPath = [self.collectionView qmui_indexPathForItemAtView:checkboxButton]; + NSIndexPath *indexPath = [_collectionView qmui_indexPathForItemAtView:checkboxButton]; if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewController:shouldCheckImageAtIndex:)] && ![self.imagePickerViewControllerDelegate imagePickerViewController:self shouldCheckImageAtIndex:indexPath.item]) { return; } - QMUIImagePickerCollectionViewCell *cell = (QMUIImagePickerCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; + QMUIImagePickerCollectionViewCell *cell = (QMUIImagePickerCollectionViewCell *)[_collectionView cellForItemAtIndexPath:indexPath]; QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:indexPath.item]; if (cell.checked) { // 移除选中状态 @@ -486,7 +525,7 @@ - (void)updateImageCountAndCheckLimited { - (void)requestImageWithIndexPath:(NSIndexPath *)indexPath { // 发出请求获取大图,如果图片在 iCloud,则会发出网络请求下载图片。这里同时保存请求 id,供取消请求使用 QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:indexPath.item]; - QMUIImagePickerCollectionViewCell *cell = (QMUIImagePickerCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; + QMUIImagePickerCollectionViewCell *cell = (QMUIImagePickerCollectionViewCell *)[_collectionView cellForItemAtIndexPath:indexPath]; imageAsset.requestID = [imageAsset requestOriginImageWithCompletion:^(UIImage *result, NSDictionary *info) { BOOL downloadSucceed = (result && !info) || (![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue]); diff --git a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h index 64362020..a0b3b796 100644 --- a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h +++ b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h @@ -1,13 +1,13 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // -// QMUINavigationBar+Transition.h +// UINavigationBar+Transition.h // qmui // // Created by QMUI Team on 11/25/16. @@ -18,6 +18,18 @@ @interface UINavigationBar (Transition) /// 用来模仿真的navBar,配合 UINavigationController+NavigationBarTransition 在转场过程中存在的一条假navBar -@property(nonatomic, strong) UINavigationBar *transitionNavigationBar; +@property(nonatomic, weak) UINavigationBar *qmuinb_copyStylesToBar; +@end + +@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/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m index 93863698..3e1c5ea7 100644 --- a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m +++ b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m @@ -1,13 +1,13 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // -// QMUINavigationBar+Transition.m +// UINavigationBar+Transition.m // qmui // // Created by QMUI Team on 11/25/16. @@ -15,32 +15,267 @@ #import "UINavigationBar+Transition.h" #import "QMUICore.h" +#import "UINavigationBar+QMUI.h" +#import "UINavigationBar+QMUIBarProtocol.h" +#import "QMUIWeakObjectContainer.h" +#import "UIImage+QMUI.h" @implementation UINavigationBar (Transition) -QMUISynthesizeIdStrongProperty(transitionNavigationBar, setTransitionNavigationBar) - + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setShadowImage:), UIImage *, ^(UINavigationBar *selfObject, UIImage *firstArgv) { - if (selfObject.transitionNavigationBar) { - selfObject.transitionNavigationBar.shadowImage = firstArgv; - } + + if (@available(iOS 15.0, *)) { + + 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; + } + }; + }); + + 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; + } + }; + }); + } + + 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; + } + }; + }); + + 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; + } + }; }); - ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setBarTintColor:), UIColor *, ^(UINavigationBar *selfObject, UIColor *firstArgv) { - if (selfObject.transitionNavigationBar) { - selfObject.transitionNavigationBar.barTintColor = firstArgv; - } + OverrideImplementation([UINavigationBar class], @selector(setBackgroundImage:forBarMetrics:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UIImage *image, UIBarMetrics barMetrics) { + + // call super + void (*originSelectorIMP)(id, SEL, UIImage *, UIBarMetrics); + originSelectorIMP = (void (*)(id, SEL, UIImage *, UIBarMetrics))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, image, barMetrics); + + if (selfObject.qmuinb_copyStylesToBar) { + [selfObject.qmuinb_copyStylesToBar setBackgroundImage:image forBarMetrics:barMetrics]; + } + }; }); - ExtendImplementationOfVoidMethodWithTwoArguments([UINavigationBar class], @selector(setBackgroundImage:forBarMetrics:), UIImage *, UIBarMetrics, ^(UINavigationBar *selfObject, UIImage *backgroundImage, UIBarMetrics barMetrics) { - if (selfObject.transitionNavigationBar) { - [selfObject.transitionNavigationBar setBackgroundImage:backgroundImage forBarMetrics:barMetrics]; - } + 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)) { + return ^(UINavigationBar *selfObject, UIBlurEffect *firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, UIBlurEffect *); + originSelectorIMP = (void (*)(id, SEL, UIBlurEffect *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if (selfObject.qmuinb_copyStylesToBar) { + selfObject.qmuinb_copyStylesToBar.qmui_effect = firstArgv; + } + }; + }); + + OverrideImplementation([UINavigationBar class], @selector(setQmui_effectForegroundColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UIColor *firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if (selfObject.qmuinb_copyStylesToBar) { + selfObject.qmuinb_copyStylesToBar.qmui_effectForegroundColor = firstArgv; + } + }; }); }); } +static char kAssociatedObjectKey_copyStylesToBar; +- (void)setQmuinb_copyStylesToBar:(UINavigationBar *)copyStylesToBar { + QMUIWeakObjectContainer *weakContainer = objc_getAssociatedObject(self, &kAssociatedObjectKey_copyStylesToBar); + if (!weakContainer) { + weakContainer = [[QMUIWeakObjectContainer alloc] init]; + } + weakContainer.object = copyStylesToBar; + objc_setAssociatedObject(self, &kAssociatedObjectKey_copyStylesToBar, weakContainer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + if (!copyStylesToBar) return; + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + copyStylesToBar.standardAppearance = self.standardAppearance; + copyStylesToBar.scrollEdgeAppearance = self.scrollEdgeAppearance; + } else { +#endif + UIImage *backgroundImage = [self backgroundImageForBarMetrics:UIBarMetricsDefault]; + if (backgroundImage && backgroundImage.size.width <= 0 && backgroundImage.size.height <= 0) { + // 假设这里的图片时通过`[UIImage new]`这种形式创建的,那么会navBar会奇怪地显示为系统默认navBar的样式。不知道为什么 navController 设置自己的 navBar 为 [UIImage new] 却没事,所以这里做个保护。 + backgroundImage = [UIImage qmui_imageWithColor:UIColorClear]; + } + [copyStylesToBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault]; + + copyStylesToBar.shadowImage = self.shadowImage; + + if (copyStylesToBar.barStyle != self.barStyle) { + copyStylesToBar.barStyle = self.barStyle; + } + + // setTranslucent 要在 setBackgroundImage 之后,因为 setBackgroundImage 内部会改变 translucent 的值 + if (copyStylesToBar.translucent != self.translucent) { + copyStylesToBar.translucent = self.translucent; + } + + if (![copyStylesToBar.barTintColor isEqual:self.barTintColor]) { + copyStylesToBar.barTintColor = self.barTintColor; + } + +#ifdef IOS15_SDK_ALLOWED + } +#endif + + copyStylesToBar.qmui_effect = self.qmui_effect; + copyStylesToBar.qmui_effectForegroundColor = self.qmui_effectForegroundColor; +} + +- (UINavigationBar *)qmuinb_copyStylesToBar { + return (UINavigationBar *)((QMUIWeakObjectContainer *)objc_getAssociatedObject(self, &kAssociatedObjectKey_copyStylesToBar)).object; +} + +@end + +@implementation _QMUITransitionNavigationBar + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // iOS 14 开启 customNavigationBarTransitionKey 的情况下转场效果错误 + // https://github.com/Tencent/QMUI_iOS/issues/1081 + if (@available(iOS 14.0, *)) { + // - [UINavigationBar _accessibility_navigationController] + OverrideImplementation([_QMUITransitionNavigationBar class], NSSelectorFromString([NSString stringWithFormat:@"_%@_%@", @"accessibility", @"navigationController"]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UINavigationController *(_QMUITransitionNavigationBar *selfObject) { + if (selfObject.originalNavigationBar) { + BeginIgnorePerformSelectorLeaksWarning + return [selfObject.originalNavigationBar performSelector:originCMD]; + EndIgnorePerformSelectorLeaksWarning + } + + // call super + UINavigationController *(*originSelectorIMP)(id, SEL); + originSelectorIMP = (UINavigationController *(*)(id, SEL))originalIMPProvider(); + UINavigationController *result = originSelectorIMP(selfObject, originCMD); + return result; + }; + }); + } + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + // - [UINavigationBar _didMoveFromWindow:toWindow:] + OverrideImplementation([_QMUITransitionNavigationBar class], NSSelectorFromString(@"_didMoveFromWindow:toWindow:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(_QMUITransitionNavigationBar *selfObject, UIWindow *firstArgv, UIWindow *secondArgv) { + + if (selfObject.shouldPreventAppearance) { + return; + } + + // call super + void (*originSelectorIMP)(id, SEL, UIWindow *, UIWindow *); + originSelectorIMP = (void (*)(id, SEL, UIWindow *, UIWindow *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); + }; + }); + } +#endif + + }); +} + +- (void)setOriginalNavigationBar:(UINavigationBar *)originBar { + _originalNavigationBar = originBar; + + // 只复制当前 originBar 的样式,所以复制完立马就清空 + originBar.qmuinb_copyStylesToBar = self; + originBar.qmuinb_copyStylesToBar = nil; + + [self updateLayout]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + // 实测 iOS 11 Beta 1-5 里,自己 init 的 navigationBar.backgroundView.height 默认一直是 44,所以才加上这个兼容 + self.qmui_backgroundView.frame = self.bounds; +} + +// NavBarRemoveBackgroundEffectAutomatically 在开启了 AutomaticCustomNavigationBarTransitionStyle 时可能对假 bar 无效 +// https://github.com/Tencent/QMUI_iOS/issues/1330 +- (void)didAddSubview:(UIView *)subview { + [super didAddSubview:subview]; + if (subview == self.qmui_backgroundView) { + [subview qmui_performSelector:NSSelectorFromString(@"updateBackground") withArguments:nil]; + } +} + +- (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/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.h b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.h index fa807c4f..c42103e8 100644 --- a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.h +++ b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UINavigationController+NavigationBarTransition.h diff --git a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m index 5500abda..21982331 100644 --- a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m +++ b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UINavigationController+NavigationBarTransition.m @@ -20,34 +20,20 @@ #import "UIImage+QMUI.h" #import "UIViewController+QMUI.h" #import "UINavigationBar+Transition.h" -#import "QMUICommonViewController.h" #import "QMUINavigationTitleView.h" #import "UINavigationBar+QMUI.h" +#import "UINavigationBar+QMUIBarProtocol.h" #import "UIView+QMUI.h" #import "QMUILog.h" -@interface _QMUITransitionNavigationBar : UINavigationBar - -@end - -@implementation _QMUITransitionNavigationBar - -- (void)layoutSubviews { - [super layoutSubviews]; - if (@available(iOS 11, *)) { - // iOS 11 以前,自己 init 的 navigationBar,它的 backgroundView 默认会一直保持与 navigationBar 的高度相等,但 iOS 11 Beta 1-5 里,自己 init 的 navigationBar.backgroundView.height 默认一直是 44,所以才加上这个兼容 - self.qmui_backgroundView.frame = self.bounds; - } -} - -@end - /** * 为了响应NavigationBarTransition分类的功能,UIViewController需要做一些相应的支持。 * @see UINavigationController+NavigationBarTransition.h */ @interface UIViewController (NavigationBarTransition) +@property(nonatomic, assign) BOOL qmuinb_shouldShowTransitionBar; + /// 用来模仿真的navBar的,在转场过程中存在的一条假navBar @property(nonatomic, strong) _QMUITransitionNavigationBar *transitionNavigationBar; @@ -57,11 +43,52 @@ @interface UIViewController (NavigationBarTransition) /// 原始containerView的背景色 @property(nonatomic, strong) UIColor *originContainerViewBackgroundColor; -/// 添加假的navBar -- (void)addTransitionNavigationBarIfNeeded; +@end + +@interface UILabel (NavigationBarTransition) +@property(nonatomic, strong) UIColor *qmui_specifiedTextColor; +@end + +@implementation UILabel (NavigationBarTransition) -/// .m文件里自己赋值和使用。因为有些特殊情况下viewDidAppear之后,有可能还会调用到viewWillLayoutSubviews,导致原始的navBar隐藏,所以用这个属性做个保护。 -@property(nonatomic, assign) BOOL lockTransitionNavigationBar; +QMUISynthesizeIdStrongProperty(qmui_specifiedTextColor, setQmui_specifiedTextColor) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation(NSClassFromString(@"UIButtonLabel"), @selector(setAttributedText:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UILabel *selfObject, NSAttributedString *attributedText) { + + if (selfObject.qmui_specifiedTextColor) { + NSMutableAttributedString *mutableAttributedText = [attributedText isKindOfClass:NSMutableAttributedString.class] ? attributedText : [attributedText mutableCopy]; + [mutableAttributedText addAttributes:@{ NSForegroundColorAttributeName : selfObject.qmui_specifiedTextColor} range:NSMakeRange(0, mutableAttributedText.length)]; + attributedText = mutableAttributedText; + } + + void (*originSelectorIMP)(id, SEL, NSAttributedString *); + originSelectorIMP = (void (*)(id, SEL, NSAttributedString *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, attributedText); + }; + }); + }); +} + +@end + +@implementation UINavigationBar (NavigationBarTransition) + +/// 获取系统自带的返回按钮 Label,如果在转场时,会获取到最上面控制器的。 +- (UILabel *)qmui_backButtonLabel { + __block UILabel *backButtonLabel = nil; + [self.qmui_contentView.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) { + if ([subview isKindOfClass:NSClassFromString(@"_UIButtonBarButton")]) { + UIButton *titleButton = [subview valueForKeyPath:@"visualProvider.titleButton"]; + backButtonLabel = titleButton.titleLabel; + *stop = YES; + } + }]; + return backButtonLabel; +} @end @@ -74,62 +101,68 @@ + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - OverrideImplementation([UIViewController class], @selector(viewWillAppear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIViewController *selfObject, BOOL firstArgv) { + ExtendImplementationOfVoidMethodWithoutArguments([UINavigationController class], @selector(qmui_didInitialize), ^(UINavigationController *selfObject) { + [selfObject qmui_addNavigationActionDidChangeBlock:^(QMUINavigationAction action, BOOL animated, __kindof UINavigationController * _Nullable weakNavigationController, __kindof UIViewController * _Nullable appearingViewController, NSArray<__kindof UIViewController *> * _Nullable disappearingViewControllers) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - // 放在最前面,留一个时机给业务可以覆盖 - [selfObject renderNavigationStyleInViewController:selfObject animated:firstArgv]; + // 左右两个界面都必须存在 + UIViewController *disappearingViewController = disappearingViewControllers.lastObject; + if (!appearingViewController || !disappearingViewController) { + return; } - // call super - void (*originSelectorIMP)(id, SEL, BOOL); - originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, firstArgv); - }; - }); - - OverrideImplementation([UIViewController class], @selector(viewDidAppear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIViewController *selfObject, BOOL firstArgv) { - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - selfObject.lockTransitionNavigationBar = YES; - - if (selfObject.transitionNavigationBar) { - - [UIViewController replaceStyleForNavigationBar:selfObject.transitionNavigationBar withNavigationBar:selfObject.navigationController.navigationBar]; - [selfObject removeTransitionNavigationBar]; - - id transitionCoordinator = selfObject.transitionCoordinator; - [transitionCoordinator containerView].backgroundColor = selfObject.originContainerViewBackgroundColor; + switch (action) { + case QMUINavigationActionDidPush: + case QMUINavigationActionWillPop: + case QMUINavigationActionDidSet: { + BOOL shouldCustomNavigationBarTransition = + [weakNavigationController shouldCustomTransitionAutomaticallyForOperation:UINavigationControllerOperationPush firstViewController:disappearingViewController secondViewController:appearingViewController]; + if (shouldCustomNavigationBarTransition) { + disappearingViewController.qmuinb_shouldShowTransitionBar = YES; + appearingViewController.qmuinb_shouldShowTransitionBar = YES; + + // 只绑定即将显示的 vc 的 bar,注意可能在 setNavigationBarHidden: 里被覆盖,引起下述问题: + // https://github.com/Tencent/QMUI_iOS/issues/1335 + weakNavigationController.navigationBar.qmuinb_copyStylesToBar = appearingViewController.transitionNavigationBar; + } } - - if ([selfObject.navigationController.viewControllers containsObject:selfObject]) { - // 防止一些 childViewController 走到这里 - selfObject.prefersNavigationBarBackgroundViewHidden = NO; + break; + case QMUINavigationActionPushCompleted: + case QMUINavigationActionPopCompleted: + case QMUINavigationActionSetCompleted: { + disappearingViewController.qmuinb_shouldShowTransitionBar = NO; + appearingViewController.qmuinb_shouldShowTransitionBar = NO; + weakNavigationController.navigationBar.qmuinb_copyStylesToBar = nil; } + break; + default: + break; } + }]; + }); + + OverrideImplementation([UINavigationController class], @selector(setNavigationBarHidden:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationController *selfObject, BOOL hidden, BOOL animated) { // call super - void (*originSelectorIMP)(id, SEL, BOOL); - originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, firstArgv); + void (*originSelectorIMP)(id, SEL, BOOL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, hidden, animated); + + if ((selfObject.qmui_isPushing || selfObject.qmui_isPopping) && selfObject.topViewController.qmuinb_shouldShowTransitionBar) { + if (hidden) { + [selfObject.topViewController removeTransitionNavigationBar]; + } else { + [selfObject.topViewController addTransitionNavigationBarAndBindNavigationBar:YES]; + } + } }; }); - OverrideImplementation([UIViewController class], @selector(viewDidDisappear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + OverrideImplementation([UIViewController class], @selector(viewWillAppear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIViewController *selfObject, BOOL firstArgv) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - selfObject.lockTransitionNavigationBar = NO; - - if (selfObject.transitionNavigationBar) { - [selfObject removeTransitionNavigationBar]; - } - } + // 放在最前面,留一个时机给业务可以覆盖 + [selfObject renderNavigationBarStyleAnimated:firstArgv]; // call super void (*originSelectorIMP)(id, SEL, BOOL); @@ -140,42 +173,7 @@ + (void)load { OverrideImplementation([UIViewController class], @selector(viewWillLayoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIViewController *selfObject) { - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (![selfObject.navigationController.delegate respondsToSelector:@selector(navigationController:animationControllerForOperation:fromViewController:toViewController:)]) { - - id transitionCoordinator = selfObject.transitionCoordinator; - UIViewController *fromViewController = [transitionCoordinator viewControllerForKey:UITransitionContextFromViewControllerKey]; - UIViewController *toViewController = [transitionCoordinator viewControllerForKey:UITransitionContextToViewControllerKey]; - - BOOL isCurrentToViewController = (selfObject == selfObject.navigationController.viewControllers.lastObject && selfObject == toViewController); - - if (isCurrentToViewController && !selfObject.lockTransitionNavigationBar) { - - BOOL shouldCustomNavigationBarTransition = NO; - - if (!selfObject.transitionNavigationBar) { - - if ([selfObject shouldCustomTransitionAutomaticallyWithFirstViewController:fromViewController secondViewController:toViewController]) { - shouldCustomNavigationBarTransition = YES; - } - - if (shouldCustomNavigationBarTransition) { - if (selfObject.navigationController.navigationBar.translucent) { - // 如果原生bar是半透明的,需要给containerView加个背景色,否则有可能会看到下面的默认黑色背景色 - toViewController.originContainerViewBackgroundColor = [transitionCoordinator containerView].backgroundColor; - [transitionCoordinator containerView].backgroundColor = [selfObject containerViewBackgroundColor]; - } - [selfObject addTransitionNavigationBarIfNeeded]; - [selfObject resizeTransitionNavigationBarFrame]; - selfObject.navigationController.navigationBar.transitionNavigationBar = selfObject.transitionNavigationBar; - selfObject.prefersNavigationBarBackgroundViewHidden = YES; - } - } - } - } - } + [selfObject.transitionNavigationBar updateLayout]; // call super void (*originSelectorIMP)(id, SEL); @@ -183,174 +181,249 @@ + (void)load { originSelectorIMP(selfObject, originCMD); }; }); + + // 修复 UISearchController push 到导航栏隐藏的界面时,会强制把导航栏重新显示出来的 bug + // https://github.com/Tencent/QMUI_iOS/issues/479 + // _navigationControllerWillShowViewController: + SEL selector = NSSelectorFromString([NSString stringWithFormat:@"_%@%@:", @"navigationController", @"WillShowViewController"]); + QMUIAssert([[UISearchController class] instancesRespondToSelector:selector], @"UIViewController (NavigationBarTransition)", @"iOS 版本更新导致 UISearchController 无法响应方法 %@", NSStringFromSelector(selector)); + OverrideImplementation([UISearchController class], selector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISearchController *selfObject, NSNotification *firstArgv) { + UIViewController *nextViewController = firstArgv.userInfo[@"UINavigationControllerNextVisibleViewController"]; + if (![nextViewController canCustomNavigationBarTransitionIfBarHiddenable]) { + void (*originSelectorIMP)(id, SEL, NSNotification *); + originSelectorIMP = (void (*)(id, SEL, NSNotification *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + } + }; + }); + + if (@available(iOS 15.0, *)) { + // - [UINavigationBar didMoveToWindow] + OverrideImplementation([UINavigationBar class], @selector(didMoveToWindow), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject) { + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + // 由于 renderNavigationBarStyleAnimated: 里对导航栏尚未添加到 window 上(UIAppearance 尚未被应用)的情况,跳过了 renderNavigationBarAppearanceAnimated:,所以这里在导航栏添加到 window 上时刷新一下导航栏样式 + // https://github.com/Tencent/QMUI_iOS/issues/1437 + if (selfObject.window) { + UINavigationController *nav = (UINavigationController *)selfObject.qmui_viewController; + if (![nav isKindOfClass:UINavigationController.class]) return; + UIViewController *topViewController = nav.topViewController; + if (topViewController.qmui_visibleState & QMUIViewControllerVisible) {// 加上这个 visibleState 的判断是因为一个普通的 UINavigationController 被初始化后导航栏默认就有一个 didMoveToWindow 的时机,这个时机里 topViewController 尚未触发 viewWillAppear:,如果不判断 visibleState,就会导致在过早的时候去设置导航栏样式,然后 viewWillAppear: 时又设置了一次。 + [topViewController renderNavigationBarAppearanceAnimated:NO]; + } + } + }; + }); + } }); } -- (void)addTransitionNavigationBarIfNeeded { - - if (!self.view.qmui_visible || !self.navigationController.navigationBar) { +- (void)addTransitionNavigationBarAndBindNavigationBar:(BOOL)shouldBind { + // add 时虽然过滤了 navigationBarHidden 的条件,但可能在 push/pop 时,新界面暂时还没刷新导航栏的显隐状态,所以还是需要在 viewWillLayoutSubviews 那边再重新根据 navigationBarHidden 的值来决定是否隐藏假 bar + if (!self.qmuinb_shouldShowTransitionBar || self.transitionNavigationBar || !self.navigationController.navigationBar || self.navigationController.navigationBarHidden) { return; } - UINavigationBar *originBar = self.navigationController.navigationBar; _QMUITransitionNavigationBar *customBar = [[_QMUITransitionNavigationBar alloc] init]; + customBar.parentViewController = self; + self.transitionNavigationBar = customBar; - if (customBar.barStyle != originBar.barStyle) { - customBar.barStyle = originBar.barStyle; + // iOS 15 里,假 bar 在 add 到界面上时会被强制同步为 UIAppearance 的值,不管你之前是否设置过自己的样式。而且在那个 runloop 内不管你后续怎么更新 standardAppearance,都会呈现出 UIAppearance 里的统一的值的样式。所以这里一方面屏蔽 didMoveToWindow,从而避免在这时候应用 UIAppearance,另一方面要保证先 add 到界面上再同步当前导航栏的样式。 + // 经测试只有 push 或 push 动画的 set 需要这么处理,pop 及 pop 动画的 set 没问题 + // iOS 14 及以下没这种问题。 + // https://github.com/Tencent/QMUI_iOS/issues/1501 + if (@available(iOS 15.0, *)) { + 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; + } } - - if (customBar.translucent != originBar.translucent) { - customBar.translucent = originBar.translucent; + [self.view addSubview:customBar]; + customBar.originalNavigationBar = self.navigationController.navigationBar;// 注意这里内部不会保留真 bar 和假 bar 的 copy 关系 + if (shouldBind) { + self.navigationController.navigationBar.qmuinb_copyStylesToBar = customBar; } - - if (![customBar.barTintColor isEqual:originBar.barTintColor]) { - customBar.barTintColor = originBar.barTintColor; +} + +- (void)removeTransitionNavigationBar { + if (self.transitionNavigationBar) { + [self.transitionNavigationBar removeFromSuperview]; + self.transitionNavigationBar = nil; + id transitionCoordinator = self.transitionCoordinator; + if (self.navigationController.navigationBar.translucent && self.originContainerViewBackgroundColor) { + [transitionCoordinator containerView].backgroundColor = self.originContainerViewBackgroundColor; + } } +} + +#pragma mark - 工具方法 + +// 根据当前的viewController,统一处理导航栏的显隐、样式 +- (void)renderNavigationBarStyleAnimated:(BOOL)animated { - UIImage *backgroundImage = [originBar backgroundImageForBarMetrics:UIBarMetricsDefault]; - if (backgroundImage && backgroundImage.size.width <= 0 && backgroundImage.size.height <= 0) { - // 假设这里的图片时通过`[UIImage new]`这种形式创建的,那么会navBar会奇怪地显示为系统默认navBar的样式。不知道为什么 navController 设置自己的 navBar 为 [UIImage new] 却没事,所以这里做个保护。 - backgroundImage = [UIImage qmui_imageWithColor:UIColorClear]; + // 屏蔽不处于 UINavigationController 里的 viewController,以及 custom containerViewController 里的 childViewController + if (![self.navigationController.viewControllers containsObject:self]) { + return; } - [customBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault]; - [customBar setShadowImage:originBar.shadowImage]; + if (![self conformsToProtocol:@protocol(QMUINavigationControllerAppearanceDelegate)]) { + return; + } - self.transitionNavigationBar = customBar; - [self resizeTransitionNavigationBarFrame]; + // 以下用于控制 vc 的外观样式,如果某个方法有实现则用方法的返回值,否则再看配置表对应的值是否有配置,有配置就使用配置表,没配置则什么都不做,维持系统原生样式 + UIViewController *vc = (UIViewController *)self; + UINavigationController *navigationController = vc.navigationController; - if (!self.navigationController.navigationBarHidden) { - [self.view addSubview:self.transitionNavigationBar]; + // 显示/隐藏 导航栏 + if ([vc canCustomNavigationBarTransitionIfBarHiddenable]) { + if ([vc hideNavigationBarWhenTransitioning]) { + if (!navigationController.isNavigationBarHidden) { + [navigationController setNavigationBarHidden:YES animated:animated]; + } + } else { + if (navigationController.isNavigationBarHidden) { + [navigationController setNavigationBarHidden:NO animated:animated]; + } + } } - CGRect viewRect = [self.navigationController.view convertRect:self.view.frame fromView:self.view.superview]; - if (viewRect.origin.y != 0 && self.view.clipsToBounds) { - QMUILog(@"UINavigationController+NavigationBarTransition", @"⚠️⚠️⚠️注意啦:当前界面 controller.view = %@ 布局并没有从屏幕顶部开始,可能会导致自定义导航栏转场的假 bar 看不到", self); + // 仅当导航栏被添加到 window 之后(UIAppearance 被应用之后),业务才可以设置导航栏的样式,否则在 UINavigationBar (QMUI) 里获取到的 navigationBar.standardAppearance 是系统默认的样式而不是 App 全局配置的样式,导致后续导航栏样式都是错的。 + // https://github.com/Tencent/QMUI_iOS/issues/1437 + if (@available(iOS 15.0, *)) { + if (!navigationController.navigationBar.window) { + return; + } } + + [self renderNavigationBarAppearanceAnimated:animated]; } -- (void)removeTransitionNavigationBar { - if (!self.transitionNavigationBar) { +// 仅处理导航栏的样式,不涉及显隐 +- (void)renderNavigationBarAppearanceAnimated:(BOOL)animated { + + // 屏蔽不处于 UINavigationController 里的 viewController,以及 custom containerViewController 里的 childViewController + if (![self.navigationController.viewControllers containsObject:self]) { return; } - [self.transitionNavigationBar removeFromSuperview]; - self.transitionNavigationBar = nil; -} - -- (void)resizeTransitionNavigationBarFrame { - if (!self.view.qmui_visible) { + + if (![self conformsToProtocol:@protocol(QMUINavigationControllerAppearanceDelegate)]) { return; } - UIView *backgroundView = self.navigationController.navigationBar.qmui_backgroundView; - CGRect rect = [backgroundView.superview convertRect:backgroundView.frame toView:self.view]; - self.transitionNavigationBar.frame = rect; + + // 以下用于控制 vc 的外观样式,如果某个方法有实现则用方法的返回值,否则再看配置表对应的值是否有配置,有配置就使用配置表,没配置则什么都不做,维持系统原生样式 + UIViewController *vc = (UIViewController *)self; + UINavigationController *navigationController = vc.navigationController; + + // 导航栏的背景色 + if ([vc respondsToSelector:@selector(qmui_navigationBarBarTintColor)]) { + UIColor *barTintColor = [vc qmui_navigationBarBarTintColor]; + navigationController.navigationBar.barTintColor = barTintColor; + } else if (QMUICMIActivated) { + navigationController.navigationBar.barTintColor = UINavigationBar.qmui_appearanceConfigured.barTintColor; + } + + // 导航栏的背景 + if ([vc respondsToSelector:@selector(qmui_navigationBarBackgroundImage)]) { + UIImage *backgroundImage = [vc qmui_navigationBarBackgroundImage]; + [navigationController.navigationBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault]; + } else if (QMUICMIActivated) { + [navigationController.navigationBar setBackgroundImage:[UINavigationBar.qmui_appearanceConfigured backgroundImageForBarMetrics:UIBarMetricsDefault] forBarMetrics:UIBarMetricsDefault]; + } + + // 导航栏的 style + if ([vc respondsToSelector:@selector(qmui_navigationBarStyle)]) { + UIBarStyle barStyle = [vc qmui_navigationBarStyle]; + navigationController.navigationBar.barStyle = barStyle; + } else if (QMUICMIActivated) { + navigationController.navigationBar.barStyle = UINavigationBar.qmui_appearanceConfigured.barStyle; + } + + // 导航栏底部的分隔线 + if ([vc respondsToSelector:@selector(qmui_navigationBarShadowImage)]) { + navigationController.navigationBar.shadowImage = [vc qmui_navigationBarShadowImage]; + } else if (QMUICMIActivated) { + navigationController.navigationBar.shadowImage = NavBarShadowImage; + } + + // 导航栏上控件的主题色 + UIColor *tintColor = + [vc respondsToSelector:@selector(qmui_navigationBarTintColor)] ? [vc qmui_navigationBarTintColor] : + QMUICMIActivated ? NavBarTintColor : nil; + if (tintColor) { + // https://github.com/Tencent/QMUI_iOS/issues/654 + // 改变 navigationBar.tintColor 后会同步改变返回按钮的文字颜色,在 iOS 10及以下,把修改 tintColor 的代码包裹在 animateAlongsideTransition 中能实现转场过渡,而从 iOS 11 开始不生效,现象是:修改了 navigationBar.tintColor 后,返回按钮的文字颜色瞬间变化。 + // 为了实现转场过渡,不要让返回按钮的文字瞬间变化,在转场前锁定 topViewController 所属的 backButtonLabel 颜色,这样在转场过程中改变了 navBar 的 tintColor 不会影响到他。 + if (navigationController.qmui_isPopping) { + UILabel *backButtonLabel = navigationController.navigationBar.qmui_backButtonLabel; + if (backButtonLabel) { + backButtonLabel.qmui_specifiedTextColor = backButtonLabel.textColor; + [vc qmui_animateAlongsideTransition:nil completion:^(id _Nonnull context) { + backButtonLabel.qmui_specifiedTextColor = nil; + }]; + } + } + + [vc qmui_animateAlongsideTransition:^(id _Nonnull context) { + navigationController.navigationBar.tintColor = tintColor; + } completion:nil]; + } + + // iOS 13 及以上,title 的更新只在 viewWillAppear 这里进行就可以了,但 iOS 12 及以下还要靠 popViewController 那边 + // iOS 12 及以下系统,在不使用自定义 titleView 的情况下,在 viewWillAppear 时通过修改 navigationBar.titleTextAttributes 来设置新界面的导航栏标题样式,push 时是生效的,但 pop 时右边界面的样式会覆盖左边界面的样式,所以 pop 时的 titleTextAttributes 改为在 did pop 时处理 + // 如果用自定义 titleView 则没这种问题,只是为了代码简单,时机的选择不区分是否自定义 title + [vc renderNavigationBarTitleAppearanceAnimated:animated]; } -#pragma mark - 工具方法 - -// 根据当前的viewController,统一处理导航栏底部的分隔线、状态栏的颜色 -- (void)renderNavigationStyleInViewController:(UIViewController *)viewController animated:(BOOL)animated { +// 仅处理导航栏标题 +- (void)renderNavigationBarTitleAppearanceAnimated:(BOOL)animated { // 屏蔽不处于 UINavigationController 里的 viewController,以及 custom containerViewController 里的 childViewController - if (![viewController.navigationController.viewControllers containsObject:viewController]) { + if (![self.navigationController.viewControllers containsObject:self]) { + return; + } + + if (![self conformsToProtocol:@protocol(QMUINavigationControllerAppearanceDelegate)]) { return; } // 以下用于控制 vc 的外观样式,如果某个方法有实现则用方法的返回值,否则再看配置表对应的值是否有配置,有配置就使用配置表,没配置则什么都不做,维持系统原生样式 - if ([viewController conformsToProtocol:@protocol(QMUINavigationControllerAppearanceDelegate)]) { - UIViewController *vc = (UIViewController *)viewController; - - // 控制界面的状态栏颜色 - BeginIgnoreDeprecatedWarning - if ([vc respondsToSelector:@selector(shouldSetStatusBarStyleLight)] && [vc shouldSetStatusBarStyleLight]) { - if ([[UIApplication sharedApplication] statusBarStyle] < UIStatusBarStyleLightContent) { - [QMUIHelper renderStatusBarStyleLight]; - } + UIViewController *vc = (UIViewController *)self; + UINavigationController *navigationController = vc.navigationController; + + // 导航栏title的颜色 + if ([vc respondsToSelector:@selector(qmui_titleViewTintColor)]) { + UIColor *tintColor = [vc qmui_titleViewTintColor]; + if (vc.navigationItem.titleView.qmui_useAsNavigationTitleView) { + vc.navigationItem.titleView.tintColor = tintColor; + } else if (!vc.navigationItem.titleView) { + NSMutableDictionary *titleTextAttributes = (navigationController.navigationBar.titleTextAttributes ?: @{}).mutableCopy; + titleTextAttributes[NSForegroundColorAttributeName] = tintColor; + navigationController.navigationBar.titleTextAttributes = titleTextAttributes.copy; } else { - if ([[UIApplication sharedApplication] statusBarStyle] >= UIStatusBarStyleLightContent) { - [QMUIHelper renderStatusBarStyleDark]; - } - } - EndIgnoreDeprecatedWarning - - // 显示/隐藏 导航栏 - if ([vc canCustomNavigationBarTransitionIfBarHiddenable]) { - if ([vc hideNavigationBarWhenTransitioning]) { - if (!viewController.navigationController.isNavigationBarHidden) { - [viewController.navigationController setNavigationBarHidden:YES animated:animated]; - } - } else { - if (viewController.navigationController.isNavigationBarHidden) { - [viewController.navigationController setNavigationBarHidden:NO animated:animated]; - } - } - } - - // 导航栏的背景色 - if ([vc respondsToSelector:@selector(navigationBarBarTintColor)]) { - UIColor *barTintColor = [vc navigationBarBarTintColor]; - viewController.navigationController.navigationBar.barTintColor = barTintColor; - } else if (QMUICMIActivated) { - viewController.navigationController.navigationBar.barTintColor = NavBarBarTintColor; - } - - // 导航栏的背景 - if ([vc respondsToSelector:@selector(navigationBarBackgroundImage)]) { - UIImage *backgroundImage = [vc navigationBarBackgroundImage]; - [viewController.navigationController.navigationBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault]; - } else if (QMUICMIActivated) { - [viewController.navigationController.navigationBar setBackgroundImage:NavBarBackgroundImage forBarMetrics:UIBarMetricsDefault]; - } - - // 导航栏底部的分隔线 - if ([vc respondsToSelector:@selector(navigationBarShadowImage)]) { - UIImage *shadowImage = [vc navigationBarShadowImage]; - [viewController.navigationController.navigationBar setShadowImage:shadowImage]; - } else if (QMUICMIActivated) { - [viewController.navigationController.navigationBar setShadowImage:NavBarShadowImage]; - } - - // 导航栏上控件的主题色 - UIColor *tintColor = - [vc respondsToSelector:@selector(navigationBarTintColor)] ? [vc navigationBarTintColor] : - QMUICMIActivated ? NavBarTintColor : nil; - if (tintColor) { - // 手势从 B 返回 A 过程中,取消手势,会调用 B 的 viewWillAppear,animateAlongsideTransition 在这种情况下不会生效,所以要用 qmui_poppingByInteractivePopGestureRecognizer 针对这种情况判断。 - BOOL shouldApplyTintColorTransition = (animated && ![vc qmui_poppingByInteractivePopGestureRecognizer]); - if (shouldApplyTintColorTransition) { - [viewController.transitionCoordinator animateAlongsideTransition:^ (id context) { - viewController.navigationController.navigationBar.tintColor = tintColor; - } completion:nil]; - } else { - viewController.navigationController.navigationBar.tintColor = tintColor; - } + // 设置了自定义的 navigationItem.titleView,则不处理 } - - // 导航栏title的颜色 - if ([vc respondsToSelector:@selector(titleViewTintColor)]) { - UIColor *tintColor = [vc titleViewTintColor]; - if ([vc isKindOfClass:[QMUICommonViewController class]]) { - ((QMUICommonViewController *)vc).titleView.tintColor = tintColor; - } else { - // TODO: molice 对 UIViewController 也支持修改 title 颜色 - } + } else if (QMUICMIActivated) { + UIColor *tintColor = NavBarTitleColor; + if (vc.navigationItem.titleView.qmui_useAsNavigationTitleView) { + vc.navigationItem.titleView.tintColor = tintColor; + } else if (!vc.navigationItem.titleView) { + NSMutableDictionary *titleTextAttributes = (navigationController.navigationBar.titleTextAttributes ?: @{}).mutableCopy; + titleTextAttributes[NSForegroundColorAttributeName] = tintColor; + navigationController.navigationBar.titleTextAttributes = titleTextAttributes.copy; } else { - if (QMUICMIActivated && [vc isKindOfClass:[QMUICommonViewController class]]) { - ((QMUICommonViewController *)vc).titleView.tintColor = NavBarTitleColor; - } else { - // TODO: molice 对 UIViewController 也支持修改 title 颜色 - } + // 设置了自定义的 navigationItem.titleView,则不处理 } } } -+ (void)replaceStyleForNavigationBar:(UINavigationBar *)navbarA withNavigationBar:(UINavigationBar *)navbarB { - navbarB.barStyle = navbarA.barStyle; - navbarB.barTintColor = navbarA.barTintColor; - [navbarB setShadowImage:navbarA.shadowImage]; - [navbarB setBackgroundImage:[navbarA backgroundImageForBarMetrics:UIBarMetricsDefault] forBarMetrics:UIBarMetricsDefault]; -} - - (BOOL)respondCustomNavigationBarTransitionIfBarHiddenable { BOOL respondIfBarHiddenable = NO; @@ -396,7 +469,8 @@ - (BOOL)hideNavigationBarWhenTransitioning { return NO; } -- (BOOL)shouldCustomTransitionAutomaticallyWithFirstViewController:(UIViewController *)viewController1 secondViewController:(UIViewController *)viewController2 { +// 对于有一个界面隐藏了导航栏的情况,我们也要做自定义的动画去干预,因为如果左右两个界面导航栏样式不同,你不去干预的话,push/pop 瞬间导航栏会变成即将显示的那个界面的样式,这不符合预期 +- (BOOL)shouldCustomTransitionAutomaticallyForOperation:(UINavigationControllerOperation)operation firstViewController:(UIViewController *)viewController1 secondViewController:(UIViewController *)viewController2 { UIViewController *vc1 = (UIViewController *)viewController1; UIViewController *vc2 = (UIViewController *)viewController2; @@ -405,6 +479,22 @@ - (BOOL)shouldCustomTransitionAutomaticallyWithFirstViewController:(UIViewContro return NO;// 只处理前后两个界面都是 QMUI 系列的场景 } + BOOL vc1Clips = vc1.isViewLoaded && vc1.view.clipsToBounds && vc1.qmui_navigationBarMaxYInViewCoordinator < NavigationContentTopConstant; + BOOL vc2Clips = vc2.isViewLoaded && vc2.view.clipsToBounds && vc2.qmui_navigationBarMaxYInViewCoordinator < NavigationContentTopConstant; + if (vc1Clips || vc2Clips) { + QMUILogWarn(@"UINavigationController (NavigationBarTransition)", @"因界面布局原因导致无法优化导航栏动画,vc1 = %@,maxY1 = %.0f, vc2 = %@,maxY2 = %.0f", vc1, vc1.qmui_navigationBarMaxYInViewCoordinator, vc2, vc2.qmui_navigationBarMaxYInViewCoordinator); + return NO;// 左右两个界面只要其中某个界面无法完整显示 navigationBar,都不进行动画优化 + } + + if ([vc1.navigationController.delegate respondsToSelector:@selector(navigationController:animationControllerForOperation:fromViewController:toViewController:)]) { + // 说明可能有自定义的系统转场动画 + BOOL a = [vc1 respondsToSelector:@selector(shouldCustomizeNavigationBarTransitionIfUsingCustomTransitionForOperation:fromViewController:toViewController:)] ? [vc1 shouldCustomizeNavigationBarTransitionIfUsingCustomTransitionForOperation:operation fromViewController:vc1 toViewController:vc2] : NO; + BOOL b = [vc2 respondsToSelector:@selector(shouldCustomizeNavigationBarTransitionIfUsingCustomTransitionForOperation:fromViewController:toViewController:)] ? [vc2 shouldCustomizeNavigationBarTransitionIfUsingCustomTransitionForOperation:operation fromViewController:vc1 toViewController:vc2] : NO; + if (!a && !b) { + return NO; + } + } + if ([vc1 respondsToSelector:@selector(customNavigationBarTransitionKey)] || [vc2 respondsToSelector:@selector(customNavigationBarTransitionKey)]) { NSString *key1 = [vc1 respondsToSelector:@selector(customNavigationBarTransitionKey)] ? [vc1 customNavigationBarTransitionKey] : nil; NSString *key2 = [vc2 respondsToSelector:@selector(customNavigationBarTransitionKey)] ? [vc2 customNavigationBarTransitionKey] : nil; @@ -416,8 +506,10 @@ - (BOOL)shouldCustomTransitionAutomaticallyWithFirstViewController:(UIViewContro return NO; } - UIImage *bg1 = [vc1 respondsToSelector:@selector(navigationBarBackgroundImage)] ? [vc1 navigationBarBackgroundImage] : [[UINavigationBar appearance] backgroundImageForBarMetrics:UIBarMetricsDefault]; - UIImage *bg2 = [vc2 respondsToSelector:@selector(navigationBarBackgroundImage)] ? [vc2 navigationBarBackgroundImage] : [[UINavigationBar appearance] backgroundImageForBarMetrics:UIBarMetricsDefault]; + + + UIImage *bg1 = [vc1 respondsToSelector:@selector(qmui_navigationBarBackgroundImage)] ? [vc1 qmui_navigationBarBackgroundImage] : [UINavigationBar.qmui_appearanceConfigured backgroundImageForBarMetrics:UIBarMetricsDefault]; + UIImage *bg2 = [vc2 respondsToSelector:@selector(qmui_navigationBarBackgroundImage)] ? [vc2 qmui_navigationBarBackgroundImage] : [UINavigationBar.qmui_appearanceConfigured backgroundImageForBarMetrics:UIBarMetricsDefault]; if (bg1 || bg2) { if (!bg1 || !bg2) { return YES;// 一个有一个没有,则需要自定义 @@ -427,10 +519,10 @@ - (BOOL)shouldCustomTransitionAutomaticallyWithFirstViewController:(UIViewContro } } - // 如果存在 backgroundImage,则 barTintColor 就算存在也不会被显示出来,所以这里只判断两个 backgroundImage 都不存在的时候 + // 如果存在 backgroundImage,则 barTintColor、barStyle 就算存在也不会被显示出来,所以这里只判断两个 backgroundImage 都不存在的时候 if (!bg1 && !bg2) { - UIColor *barTintColor1 = [vc1 respondsToSelector:@selector(navigationBarBarTintColor)] ? [vc1 navigationBarBarTintColor] : [UINavigationBar appearance].barTintColor; - UIColor *barTintColor2 = [vc2 respondsToSelector:@selector(navigationBarBarTintColor)] ? [vc2 navigationBarBarTintColor] : [UINavigationBar appearance].barTintColor; + UIColor *barTintColor1 = [vc1 respondsToSelector:@selector(qmui_navigationBarBarTintColor)] ? [vc1 qmui_navigationBarBarTintColor] : UINavigationBar.qmui_appearanceConfigured.barTintColor; + UIColor *barTintColor2 = [vc2 respondsToSelector:@selector(qmui_navigationBarBarTintColor)] ? [vc2 qmui_navigationBarBarTintColor] : UINavigationBar.qmui_appearanceConfigured.barTintColor; if (barTintColor1 || barTintColor2) { if (!barTintColor1 || !barTintColor2) { return YES; @@ -439,10 +531,16 @@ - (BOOL)shouldCustomTransitionAutomaticallyWithFirstViewController:(UIViewContro return YES; } } + + UIBarStyle barStyle1 = [vc1 respondsToSelector:@selector(qmui_navigationBarStyle)] ? [vc1 qmui_navigationBarStyle] : UINavigationBar.qmui_appearanceConfigured.barStyle; + UIBarStyle barStyle2 = [vc2 respondsToSelector:@selector(qmui_navigationBarStyle)] ? [vc2 qmui_navigationBarStyle] : UINavigationBar.qmui_appearanceConfigured.barStyle; + if (barStyle1 != barStyle2) { + return YES; + } } - UIImage *shadowImage1 = [vc1 respondsToSelector:@selector(navigationBarShadowImage)] ? [vc1 navigationBarShadowImage] : (vc1.navigationController.navigationBar ? vc1.navigationController.navigationBar.shadowImage : (QMUICMIActivated ? NavBarShadowImage : nil)); - UIImage *shadowImage2 = [vc2 respondsToSelector:@selector(navigationBarShadowImage)] ? [vc2 navigationBarShadowImage] : (vc2.navigationController.navigationBar ? vc2.navigationController.navigationBar.shadowImage : (QMUICMIActivated ? NavBarShadowImage : nil)); + UIImage *shadowImage1 = [vc1 respondsToSelector:@selector(qmui_navigationBarShadowImage)] ? [vc1 qmui_navigationBarShadowImage] : (vc1.navigationController.navigationBar ? vc1.navigationController.navigationBar.shadowImage : (QMUICMIActivated ? NavBarShadowImage : nil)); + UIImage *shadowImage2 = [vc2 respondsToSelector:@selector(qmui_navigationBarShadowImage)] ? [vc2 qmui_navigationBarShadowImage] : (vc2.navigationController.navigationBar ? vc2.navigationController.navigationBar.shadowImage : (QMUICMIActivated ? NavBarShadowImage : nil)); if (shadowImage1 || shadowImage2) { if (!shadowImage1 || !shadowImage2) { return YES; @@ -456,19 +554,17 @@ - (BOOL)shouldCustomTransitionAutomaticallyWithFirstViewController:(UIViewContro } - (UIColor *)containerViewBackgroundColor { - UIColor *backgroundColor = UIColorWhite; if ([self conformsToProtocol:@protocol(QMUICustomNavigationBarTransitionDelegate)]) { UIViewController *vc = (UIViewController *)self; if ([vc respondsToSelector:@selector(containerViewBackgroundColorWhenTransitioning)]) { - backgroundColor = [vc containerViewBackgroundColorWhenTransitioning]; + return [vc containerViewBackgroundColorWhenTransitioning]; } } - return backgroundColor; + return self.isViewLoaded && self.view.backgroundColor ? self.view.backgroundColor : UIColorWhite; } #pragma mark - Setter / Getter -QMUISynthesizeBOOLProperty(lockTransitionNavigationBar, setLockTransitionNavigationBar) QMUISynthesizeIdStrongProperty(transitionNavigationBar, setTransitionNavigationBar) QMUISynthesizeIdStrongProperty(originContainerViewBackgroundColor, setOriginContainerViewBackgroundColor) @@ -487,176 +583,23 @@ - (BOOL)prefersNavigationBarBackgroundViewHidden { return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_backgroundViewHidden)) boolValue]; } -@end - - -@implementation UINavigationController (NavigationBarTransition) - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - - OverrideImplementation([UINavigationController class], @selector(pushViewController:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UINavigationController *selfObject, UIViewController *viewController, BOOL animated) { - - // call super - void (^callSuperBlock)(UIViewController *, BOOL) = ^void(UIViewController *aViewController, BOOL aAnimated) { - void (*originSelectorIMP)(id, SEL, UIViewController *, BOOL); - originSelectorIMP = (void (*)(id, SEL, UIViewController *, BOOL))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, aViewController, aAnimated); - }; - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if ([selfObject.delegate respondsToSelector:@selector(navigationController:animationControllerForOperation:fromViewController:toViewController:)]) { - return callSuperBlock(viewController, animated); - } - - UIViewController *disappearingViewController = selfObject.viewControllers.lastObject; - if (!disappearingViewController) { - return callSuperBlock(viewController, animated); - } - - BOOL shouldCustomNavigationBarTransition = [selfObject shouldCustomTransitionAutomaticallyWithFirstViewController:disappearingViewController secondViewController:viewController]; - - if (shouldCustomNavigationBarTransition) { - [disappearingViewController addTransitionNavigationBarIfNeeded]; - disappearingViewController.prefersNavigationBarBackgroundViewHidden = YES; - } - } - callSuperBlock(viewController, animated); - }; - }); - - OverrideImplementation([UINavigationController class], @selector(setViewControllers:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UINavigationController *selfObject, NSArray *viewControllers, BOOL animated) { - - // call super - void (^callSuperBlock)(NSArray*, BOOL) = ^void(NSArray *aViewControllers, BOOL aAnimated) { - void (*originSelectorIMP)(id, SEL, NSArray *, BOOL); - originSelectorIMP = (void (*)(id, SEL, NSArray *, BOOL))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, aViewControllers, aAnimated); - }; - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (viewControllers.count <= 0 || !animated) { - return callSuperBlock(viewControllers, animated); - } - UIViewController *disappearingViewController = selfObject.viewControllers.lastObject; - UIViewController *appearingViewController = viewControllers.lastObject; - if (!disappearingViewController) { - return callSuperBlock(viewControllers, animated); - } - [selfObject handlePopViewControllerNavigationBarTransitionWithDisappearViewController:disappearingViewController appearViewController:appearingViewController]; - } - callSuperBlock(viewControllers, animated); - }; - }); - - OverrideImplementation([UINavigationController class], @selector(popViewControllerAnimated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^UIViewController *(UINavigationController *selfObject, BOOL animated) { - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - UIViewController *disappearingViewController = selfObject.viewControllers.lastObject; - UIViewController *appearingViewController = selfObject.viewControllers.count >= 2 ? selfObject.viewControllers[selfObject.viewControllers.count - 2] : nil; - if (disappearingViewController && appearingViewController) { - [selfObject handlePopViewControllerNavigationBarTransitionWithDisappearViewController:disappearingViewController appearViewController:appearingViewController]; - } - } - - // call super - UIViewController *(*originSelectorIMP)(id, SEL, BOOL); - originSelectorIMP = (UIViewController *(*)(id, SEL, BOOL))originalIMPProvider(); - UIViewController *result = originSelectorIMP(selfObject, originCMD, animated); - return result; - }; - }); - - OverrideImplementation([UINavigationController class], @selector(popToViewController:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^NSArray *(UINavigationController *selfObject, UIViewController *viewController, BOOL animated) { - - // call super - NSArray *(^callSuperBlock)(UIViewController *, BOOL) = ^NSArray*(UIViewController *aViewController, BOOL aAnimated) { - NSArray *(*originSelectorIMP)(id, SEL, UIViewController *, BOOL); - originSelectorIMP = (NSArray * (*)(id, SEL, UIViewController *, BOOL))originalIMPProvider(); - NSArray *result = originSelectorIMP(selfObject, originCMD, aViewController, aAnimated); - return result; - }; - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - UIViewController *disappearingViewController = selfObject.viewControllers.lastObject; - UIViewController *appearingViewController = viewController; - NSArray *poppedViewControllers = callSuperBlock(viewController, animated); - if (poppedViewControllers) { - [selfObject handlePopViewControllerNavigationBarTransitionWithDisappearViewController:disappearingViewController appearViewController:appearingViewController]; - } - return poppedViewControllers; - } - return callSuperBlock(viewController, animated); - }; - }); - - ExtendImplementationOfNonVoidMethodWithSingleArgument([UINavigationController class], @selector(popToRootViewControllerAnimated:), BOOL, NSArray *, ^NSArray *(UINavigationController *selfObject, BOOL animated, NSArray *originReturnValue) { - if (selfObject.viewControllers.count > 1) { - UIViewController *disappearingViewController = selfObject.viewControllers.lastObject; - UIViewController *appearingViewController = selfObject.viewControllers.firstObject; - if (originReturnValue) { - [selfObject handlePopViewControllerNavigationBarTransitionWithDisappearViewController:disappearingViewController appearViewController:appearingViewController]; - } - } - return originReturnValue; - }); - }); -} - -- (void)handlePopViewControllerNavigationBarTransitionWithDisappearViewController:(UIViewController *)disappearViewController appearViewController:(UIViewController *)appearViewController { - - if (![self.delegate respondsToSelector:@selector(navigationController:animationControllerForOperation:fromViewController:toViewController:)]) { - - BOOL shouldCustomNavigationBarTransition = [self shouldCustomTransitionAutomaticallyWithFirstViewController:disappearViewController secondViewController:appearViewController]; - - if (shouldCustomNavigationBarTransition) { - [disappearViewController addTransitionNavigationBarIfNeeded]; - if (appearViewController.transitionNavigationBar) { - // 假设从A→B→C,其中A设置了bar的样式,B跟随A所以B里没有设置bar样式的代码,C又把样式改为另一种,此时从C返回B时,由于B没有设置bar的样式的代码,所以bar的样式依然会保留C的,这就错了,所以每次都要手动改回来才保险 - [UIViewController replaceStyleForNavigationBar:appearViewController.transitionNavigationBar withNavigationBar:self.navigationBar]; - } - disappearViewController.prefersNavigationBarBackgroundViewHidden = YES; +static char kAssociatedObjectKey_shouldShowTransitionBar; +- (void)setQmuinb_shouldShowTransitionBar:(BOOL)shouldShowTransitionBar { + objc_setAssociatedObject(self, &kAssociatedObjectKey_shouldShowTransitionBar, @(shouldShowTransitionBar), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (shouldShowTransitionBar) { + [self addTransitionNavigationBarAndBindNavigationBar:NO];// 这里不绑定 bar,因为不知道此时是两个 vc 里的哪一个 + self.prefersNavigationBarBackgroundViewHidden = YES; + } else { + [self removeTransitionNavigationBar]; + // 屏蔽一些 childViewController 触发的场景,只关心堆栈里的 + if ([self.navigationController.viewControllers containsObject:self]) { + self.prefersNavigationBarBackgroundViewHidden = NO; } - } } -@end - -@interface UISearchController (NavigationBarTransition) - -@end - -@implementation UISearchController (NavigationBarTransition) - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - - // 修复 UISearchController push 到导航栏隐藏的界面时,会强制把导航栏重新显示出来的 bug - // https://github.com/Tencent/QMUI_iOS/issues/479 - SEL selector = NSSelectorFromString([NSString stringWithFormat:@"_%@%@:", @"navigationController", @"WillShowViewController"]); - NSAssert([[self class] instancesRespondToSelector:selector], @"iOS 版本更新导致 UISearchController 无法响应方法 %@", NSStringFromSelector(selector)); - OverrideImplementation([self class], selector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UISearchController *selfObject, NSNotification *firstArgv) { - UIViewController *nextViewController = firstArgv.userInfo[@"UINavigationControllerNextVisibleViewController"]; - if (![selfObject isKindOfClass:originClass] || ![nextViewController canCustomNavigationBarTransitionIfBarHiddenable]) { - void (*originSelectorIMP)(id, SEL, NSNotification *); - originSelectorIMP = (void (*)(id, SEL, NSNotification *))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, firstArgv); - } - }; - }); - }); +- (BOOL)qmuinb_shouldShowTransitionBar { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_shouldShowTransitionBar)) boolValue]; } @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.h b/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.h index f48b5530..ca1a82be 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIAlertController.h @@ -93,7 +93,7 @@ typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) { UIView *_scrollWrapView; // 包含上下两个 scrollView 的容器 UIScrollView *_headerScrollView; // 上半部分的内容的 scrollView,例如 title、message UIScrollView *_buttonScrollView; // 所有按钮的容器,特别的,actionSheet 下的取消按钮不放在这里面,因为它不参与滚动 - UIControl *_maskView; // 背后占满整个屏幕的半透明黑色遮罩 + UIControl *_dimmingView; // 背后占满整个屏幕的半透明黑色遮罩 } /// alert距离屏幕四边的间距,默认UIEdgeInsetsMake(0, 0, 0, 0)。alert的宽度最终是通过屏幕宽度减去水平的 alertContentMargin 和 alertContentMaximumWidth 决定的。 @@ -153,6 +153,12 @@ typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) { /// alert 内部 textField 的边框颜色,如果不需要边框,可设置为 nil @property(nullable, nonatomic, strong) UIColor *alertTextFieldBorderColor UI_APPEARANCE_SELECTOR; +/// alert 内部 textField 的 textInsets,textField 的高度会由文字大小加这个 inset 来决定 +@property(nonatomic, assign) UIEdgeInsets alertTextFieldTextInsets UI_APPEARANCE_SELECTOR; + +/// alert 内部 textField 的 margin,当存在多个 textField 时可通过参数 @c aTextFieldIndex 来为不同 textField 设置不一样的 margin。 +/// @note 注意 margin 是在原有布局基础上叠加的,左右叠加 @c alertHeaderInsets ,顶部 @c alertHeaderInsets.top ,底部为 0。 +@property(nonatomic, copy) UIEdgeInsets (^alertTextFieldMarginBlock)(__kindof QMUIAlertController *aAlertController, NSInteger aTextFieldIndex); /// sheet距离屏幕四边的间距,默认UIEdgeInsetsMake(10, 10, 10, 10)。 @property(nonatomic, assign) UIEdgeInsets sheetContentMargin UI_APPEARANCE_SELECTOR; @@ -205,6 +211,8 @@ typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) { /// sheet头部title和message之间的间距,默认8pt @property(nonatomic, assign) CGFloat sheetTitleMessageSpacing UI_APPEARANCE_SELECTOR; +/// sheet 的列数,一行显示多少个 button,默认是 1。 +@property(nonatomic, assign) CGFloat sheetButtonColumnCount UI_APPEARANCE_SELECTOR; /// 默认初始化方法 - (nonnull instancetype)initWithTitle:(nullable NSString *)title message:(nullable NSString *)message preferredStyle:(QMUIAlertControllerStyle)preferredStyle; @@ -257,10 +265,10 @@ typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) { /// 将`QMUIAlertController`弹出来的`QMUIModalPresentationViewController`对象 @property(nullable, nonatomic, strong, readonly) QMUIModalPresentationViewController *modalPresentationViewController; -/// 主体内容(alert 下指整个弹窗,actionSheet 下指取消按钮上方的那些 header 和 按钮)背后用来做背景样式的 view,默认为空白的 UIView,当你需要做磨砂效果时可以将一个 UIVisualEffectView 赋值给它(但推荐用 QMUIVisualEffectView)。当赋值为 nil 时,内部会自动创建一个空白的 UIView 代替,以保证这个属性不为空。 +/// 主体内容(alert 下指整个弹窗,actionSheet 下指取消按钮上方的那些 header 和 按钮)背后用来做背景样式的 view,默认为空白的 UIView,当你需要做磨砂效果时可以将一个 UIVisualEffectView 赋值给它。当赋值为 nil 时,内部会自动创建一个空白的 UIView 代替,以保证这个属性不为空。 @property(null_resettable, nonatomic, strong) UIView *mainVisualEffectView; -/// actionSheet 下的取消按钮背后用来做背景样式的 view,默认为空白的 UIView,当你需要做磨砂效果时可以将一个 UIVisualEffectView 赋值给它(但推荐用 QMUIVisualEffectView)。alert 情况下不会出现。当赋值为 nil 时,内部会自动创建一个空白的 UIView 代替,以保证这个属性不为空。 +/// actionSheet 下的取消按钮背后用来做背景样式的 view,默认为空白的 UIView,当你需要做磨砂效果时可以将一个 UIVisualEffectView 赋值给它。alert 情况下不会出现。当赋值为 nil 时,内部会自动创建一个空白的 UIView 代替,以保证这个属性不为空。 @property(null_resettable, nonatomic, strong) UIView *cancelButtonVisualEffectView; /** @@ -270,16 +278,13 @@ typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) { */ @property(nonatomic, assign) BOOL orderActionsByAddedOrdered; -/// maskView是否响应点击,alert默认为NO,sheet默认为YES -@property(nonatomic, assign) BOOL shouldRespondMaskViewTouch; +/// dimmingView 是否响应点击,alert 默认为NO,sheet 默认为YES +@property(nonatomic, assign) BOOL shouldRespondDimmingViewTouch; /// 在 iPhoneX 机器上是否延伸底部背景色。因为在 iPhoneX 上我们会把整个面板往上移动 safeArea 的距离,如果你的面板本来就配置成撑满全屏的样式,那么就会露出底部的空隙,isExtendBottomLayout 可以帮助你把空暇填补上。默认为NO。 /// @warning: 只对 sheet 类型有效 @property(nonatomic, assign) BOOL isExtendBottomLayout UI_APPEARANCE_SELECTOR; -/// 在显示 alert 之前先降下键盘,默认为 YES。系统的 UIAlertController 也会在显示时降下键盘,但它能在消失后把键盘自动升起,并且这个过程不会触发 becomeFirstResponder/resignFirstResponder,QMUIAlertController 暂时做不到这样的效果,只负责降下,不负责恢复。 -@property(nonatomic, assign) BOOL dismissKeyboardAutomatically; - @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.m b/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.m index a84f74d1..32a201a2 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIAlertController.m @@ -23,6 +23,8 @@ #import "UIImage+QMUI.h" #import "CALayer+QMUI.h" #import "QMUIKeyboardManager.h" +#import "QMUIAppearance.h" +#import "QMUILabel.h" static NSUInteger alertControllerCount = 0; @@ -75,7 +77,7 @@ @interface QMUIAlertAction () @implementation QMUIAlertAction + (nonnull instancetype)actionWithTitle:(nullable NSString *)title style:(QMUIAlertActionStyle)style handler:(void (^)(__kindof QMUIAlertController *, QMUIAlertAction *))handler { - QMUIAlertAction *alertAction = [[QMUIAlertAction alloc] init]; + QMUIAlertAction *alertAction = [[self alloc] init]; alertAction.title = title; alertAction.style = style; alertAction.handler = handler; @@ -111,66 +113,59 @@ - (void)handleAlertActionEvent:(id)sender { @implementation QMUIAlertController (UIAppearance) -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self appearance]; - }); ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; } -static QMUIAlertController *alertControllerAppearance; -+ (nonnull instancetype)appearance { ++ (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - [self resetAppearance]; + [self initAppearance]; }); - return alertControllerAppearance; } -+ (void)resetAppearance { - if (!alertControllerAppearance) { - - alertControllerAppearance = [[QMUIAlertController alloc] init]; - - alertControllerAppearance.alertContentMargin = UIEdgeInsetsMake(0, 0, 0, 0); - alertControllerAppearance.alertContentMaximumWidth = 270; - alertControllerAppearance.alertSeparatorColor = UIColorMake(211, 211, 219); - alertControllerAppearance.alertTitleAttributes = @{NSForegroundColorAttributeName:UIColorBlack,NSFontAttributeName:UIFontBoldMake(17),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]}; - alertControllerAppearance.alertMessageAttributes = @{NSForegroundColorAttributeName:UIColorBlack,NSFontAttributeName:UIFontMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]}; - alertControllerAppearance.alertButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)}; - alertControllerAppearance.alertButtonDisabledAttributes = @{NSForegroundColorAttributeName:UIColorMake(129, 129, 129),NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)}; - alertControllerAppearance.alertCancelButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontBoldMake(17),NSKernAttributeName:@(0)}; - alertControllerAppearance.alertDestructiveButtonAttributes = @{NSForegroundColorAttributeName:UIColorRed,NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)}; - alertControllerAppearance.alertContentCornerRadius = 13; - alertControllerAppearance.alertButtonHeight = 44; - alertControllerAppearance.alertHeaderBackgroundColor = UIColorMakeWithRGBA(247, 247, 247, 1); - alertControllerAppearance.alertButtonBackgroundColor = alertControllerAppearance.alertHeaderBackgroundColor; - alertControllerAppearance.alertButtonHighlightBackgroundColor = UIColorMake(232, 232, 232); - alertControllerAppearance.alertHeaderInsets = UIEdgeInsetsMake(20, 16, 20, 16); - alertControllerAppearance.alertTitleMessageSpacing = 3; - alertControllerAppearance.alertTextFieldFont = UIFontMake(14); - alertControllerAppearance.alertTextFieldTextColor = UIColorBlack; - alertControllerAppearance.alertTextFieldBorderColor = UIColorMake(210, 210, 210); - - alertControllerAppearance.sheetContentMargin = UIEdgeInsetsMake(10, 10, 10, 10); - alertControllerAppearance.sheetContentMaximumWidth = [QMUIHelper screenSizeFor55Inch].width - UIEdgeInsetsGetHorizontalValue(alertControllerAppearance.sheetContentMargin); - alertControllerAppearance.sheetSeparatorColor = UIColorMake(211, 211, 219); - alertControllerAppearance.sheetTitleAttributes = @{NSForegroundColorAttributeName:UIColorMake(143, 143, 143),NSFontAttributeName:UIFontBoldMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]}; - alertControllerAppearance.sheetMessageAttributes = @{NSForegroundColorAttributeName:UIColorMake(143, 143, 143),NSFontAttributeName:UIFontMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]}; - alertControllerAppearance.sheetButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)}; - alertControllerAppearance.sheetButtonDisabledAttributes = @{NSForegroundColorAttributeName:UIColorMake(129, 129, 129),NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)}; - alertControllerAppearance.sheetCancelButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontBoldMake(20),NSKernAttributeName:@(0)}; - alertControllerAppearance.sheetDestructiveButtonAttributes = @{NSForegroundColorAttributeName:UIColorRed,NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)}; - alertControllerAppearance.sheetCancelButtonMarginTop = 8; - alertControllerAppearance.sheetContentCornerRadius = 13; - alertControllerAppearance.sheetButtonHeight = 57; - alertControllerAppearance.sheetHeaderBackgroundColor = UIColorMakeWithRGBA(247, 247, 247, 1); - alertControllerAppearance.sheetButtonBackgroundColor = alertControllerAppearance.sheetHeaderBackgroundColor; - alertControllerAppearance.sheetButtonHighlightBackgroundColor = UIColorMake(232, 232, 232); - alertControllerAppearance.sheetHeaderInsets = UIEdgeInsetsMake(16, 16, 16, 16); - alertControllerAppearance.sheetTitleMessageSpacing = 8; - alertControllerAppearance.isExtendBottomLayout = NO; - } ++ (void)initAppearance { + QMUIAlertController *alertControllerAppearance = QMUIAlertController.appearance; + alertControllerAppearance.alertContentMargin = UIEdgeInsetsMake(0, 0, 0, 0); + alertControllerAppearance.alertContentMaximumWidth = 270; + alertControllerAppearance.alertSeparatorColor = UIColorMake(211, 211, 219); + alertControllerAppearance.alertTitleAttributes = @{NSForegroundColorAttributeName:UIColorBlack,NSFontAttributeName:UIFontBoldMake(17),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]}; + alertControllerAppearance.alertMessageAttributes = @{NSForegroundColorAttributeName:UIColorBlack,NSFontAttributeName:UIFontMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]}; + alertControllerAppearance.alertButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)}; + alertControllerAppearance.alertButtonDisabledAttributes = @{NSForegroundColorAttributeName:UIColorMake(129, 129, 129),NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)}; + alertControllerAppearance.alertCancelButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontBoldMake(17),NSKernAttributeName:@(0)}; + alertControllerAppearance.alertDestructiveButtonAttributes = @{NSForegroundColorAttributeName:UIColorRed,NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)}; + alertControllerAppearance.alertContentCornerRadius = 13; + alertControllerAppearance.alertButtonHeight = 44; + alertControllerAppearance.alertHeaderBackgroundColor = UIColorMakeWithRGBA(247, 247, 247, 1); + alertControllerAppearance.alertButtonBackgroundColor = alertControllerAppearance.alertHeaderBackgroundColor; + alertControllerAppearance.alertButtonHighlightBackgroundColor = UIColorMake(232, 232, 232); + alertControllerAppearance.alertHeaderInsets = UIEdgeInsetsMake(20, 16, 20, 16); + alertControllerAppearance.alertTitleMessageSpacing = 3; + alertControllerAppearance.alertTextFieldFont = UIFontMake(14); + alertControllerAppearance.alertTextFieldTextColor = UIColorBlack; + alertControllerAppearance.alertTextFieldBorderColor = UIColorMake(210, 210, 210); + alertControllerAppearance.alertTextFieldTextInsets = UIEdgeInsetsMake(4, 7, 4, 7); + + alertControllerAppearance.sheetContentMargin = UIEdgeInsetsMake(10, 10, 10, 10); + alertControllerAppearance.sheetContentMaximumWidth = [QMUIHelper screenSizeFor55Inch].width - UIEdgeInsetsGetHorizontalValue(alertControllerAppearance.sheetContentMargin); + alertControllerAppearance.sheetSeparatorColor = UIColorMake(211, 211, 219); + alertControllerAppearance.sheetTitleAttributes = @{NSForegroundColorAttributeName:UIColorMake(143, 143, 143),NSFontAttributeName:UIFontBoldMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]}; + alertControllerAppearance.sheetMessageAttributes = @{NSForegroundColorAttributeName:UIColorMake(143, 143, 143),NSFontAttributeName:UIFontMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]}; + alertControllerAppearance.sheetButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)}; + alertControllerAppearance.sheetButtonDisabledAttributes = @{NSForegroundColorAttributeName:UIColorMake(129, 129, 129),NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)}; + alertControllerAppearance.sheetCancelButtonAttributes = @{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontBoldMake(20),NSKernAttributeName:@(0)}; + alertControllerAppearance.sheetDestructiveButtonAttributes = @{NSForegroundColorAttributeName:UIColorRed,NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)}; + alertControllerAppearance.sheetCancelButtonMarginTop = 8; + alertControllerAppearance.sheetContentCornerRadius = 13; + alertControllerAppearance.sheetButtonHeight = 57; + alertControllerAppearance.sheetHeaderBackgroundColor = UIColorMakeWithRGBA(247, 247, 247, 1); + alertControllerAppearance.sheetButtonBackgroundColor = alertControllerAppearance.sheetHeaderBackgroundColor; + alertControllerAppearance.sheetButtonHighlightBackgroundColor = UIColorMake(232, 232, 232); + alertControllerAppearance.sheetHeaderInsets = UIEdgeInsetsMake(16, 16, 16, 16); + alertControllerAppearance.sheetTitleMessageSpacing = 8; + alertControllerAppearance.sheetButtonColumnCount = 1; + alertControllerAppearance.isExtendBottomLayout = NO; } @end @@ -185,7 +180,7 @@ @interface QMUIAlertController () *alertActions; @@ -237,49 +232,14 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { } - (void)didInitialize { - if (alertControllerAppearance) { - self.alertContentMargin = [QMUIAlertController appearance].alertContentMargin; - self.alertContentMaximumWidth = [QMUIAlertController appearance].alertContentMaximumWidth; - self.alertSeparatorColor = [QMUIAlertController appearance].alertSeparatorColor; - self.alertContentCornerRadius = [QMUIAlertController appearance].alertContentCornerRadius; - self.alertTitleAttributes = [QMUIAlertController appearance].alertTitleAttributes; - self.alertMessageAttributes = [QMUIAlertController appearance].alertMessageAttributes; - self.alertButtonAttributes = [QMUIAlertController appearance].alertButtonAttributes; - self.alertButtonDisabledAttributes = [QMUIAlertController appearance].alertButtonDisabledAttributes; - self.alertCancelButtonAttributes = [QMUIAlertController appearance].alertCancelButtonAttributes; - self.alertDestructiveButtonAttributes = [QMUIAlertController appearance].alertDestructiveButtonAttributes; - self.alertButtonHeight = [QMUIAlertController appearance].alertButtonHeight; - self.alertHeaderBackgroundColor = [QMUIAlertController appearance].alertHeaderBackgroundColor; - self.alertButtonBackgroundColor = [QMUIAlertController appearance].alertButtonBackgroundColor; - self.alertButtonHighlightBackgroundColor = [QMUIAlertController appearance].alertButtonHighlightBackgroundColor; - self.alertHeaderInsets = [QMUIAlertController appearance].alertHeaderInsets; - self.alertTitleMessageSpacing = [QMUIAlertController appearance].alertTitleMessageSpacing; - self.alertTextFieldFont = [QMUIAlertController appearance].alertTextFieldFont; - self.alertTextFieldTextColor = [QMUIAlertController appearance].alertTextFieldTextColor; - self.alertTextFieldBorderColor = [QMUIAlertController appearance].alertTextFieldBorderColor; - - self.sheetContentMargin = [QMUIAlertController appearance].sheetContentMargin; - self.sheetContentMaximumWidth = [QMUIAlertController appearance].sheetContentMaximumWidth; - self.sheetSeparatorColor = [QMUIAlertController appearance].sheetSeparatorColor; - self.sheetTitleAttributes = [QMUIAlertController appearance].sheetTitleAttributes; - self.sheetMessageAttributes = [QMUIAlertController appearance].sheetMessageAttributes; - self.sheetButtonAttributes = [QMUIAlertController appearance].sheetButtonAttributes; - self.sheetButtonDisabledAttributes = [QMUIAlertController appearance].sheetButtonDisabledAttributes; - self.sheetCancelButtonAttributes = [QMUIAlertController appearance].sheetCancelButtonAttributes; - self.sheetDestructiveButtonAttributes = [QMUIAlertController appearance].sheetDestructiveButtonAttributes; - self.sheetCancelButtonMarginTop = [QMUIAlertController appearance].sheetCancelButtonMarginTop; - self.sheetContentCornerRadius = [QMUIAlertController appearance].sheetContentCornerRadius; - self.sheetButtonHeight = [QMUIAlertController appearance].sheetButtonHeight; - self.sheetHeaderBackgroundColor = [QMUIAlertController appearance].sheetHeaderBackgroundColor; - self.sheetButtonBackgroundColor = [QMUIAlertController appearance].sheetButtonBackgroundColor; - self.sheetButtonHighlightBackgroundColor = [QMUIAlertController appearance].sheetButtonHighlightBackgroundColor; - self.sheetHeaderInsets = [QMUIAlertController appearance].sheetHeaderInsets; - self.sheetTitleMessageSpacing = [QMUIAlertController appearance].sheetTitleMessageSpacing; - self.isExtendBottomLayout = [QMUIAlertController appearance].isExtendBottomLayout; - } - + [self qmui_applyAppearance]; + self.alertTextFieldMarginBlock = ^UIEdgeInsets(__kindof QMUIAlertController *aAlertController, NSInteger aTextFieldIndex) { + if (aTextFieldIndex == aAlertController.textFields.count - 1) { + return UIEdgeInsetsMake(0, 0, 16, 0); + } + return UIEdgeInsetsZero; + }; self.shouldManageTextFieldsReturnEventAutomatically = YES; - self.dismissKeyboardAutomatically = YES; } - (void)setAlertButtonAttributes:(NSDictionary *)alertButtonAttributes { @@ -375,9 +335,9 @@ - (void)setSheetHeaderBackgroundColor:(UIColor *)sheetHeaderBackgroundColor { - (void)updateHeaderBackgrondColor { if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) { - if (self.headerScrollView) { self.headerScrollView.backgroundColor = self.sheetHeaderBackgroundColor; } + if (_headerScrollView) { _headerScrollView.backgroundColor = self.sheetHeaderBackgroundColor; } } else if (self.preferredStyle == QMUIAlertControllerStyleAlert) { - if (self.headerScrollView) { self.headerScrollView.backgroundColor = self.alertHeaderBackgroundColor; } + if (_headerScrollView) { _headerScrollView.backgroundColor = self.alertHeaderBackgroundColor; } } } @@ -419,15 +379,15 @@ - (void)setIsExtendBottomLayout:(BOOL)isExtendBottomLayout { } - (void)updateExtendLayerAppearance { - if (self.extendLayer) { - self.extendLayer.backgroundColor = self.sheetButtonBackgroundColor.CGColor; + if (_extendLayer) { + _extendLayer.backgroundColor = self.sheetButtonBackgroundColor.CGColor; } } - (void)updateCornerRadius { if (self.preferredStyle == QMUIAlertControllerStyleAlert) { if (self.containerView) { self.containerView.layer.cornerRadius = self.alertContentCornerRadius; self.containerView.clipsToBounds = YES; } - if (self.cancelButtonVisualEffectView) { self.cancelButtonVisualEffectView.layer.cornerRadius = 0; self.cancelButtonVisualEffectView.clipsToBounds = NO;} + if (self.cancelButtonVisualEffectView) { self.cancelButtonVisualEffectView.layer.cornerRadius = self.alertContentCornerRadius; self.cancelButtonVisualEffectView.clipsToBounds = NO;} if (self.scrollWrapView) { self.scrollWrapView.layer.cornerRadius = 0; self.scrollWrapView.clipsToBounds = NO; } } else { if (self.containerView) { self.containerView.layer.cornerRadius = 0; self.containerView.clipsToBounds = NO; } @@ -457,6 +417,20 @@ - (void)setAlertTextFieldTextColor:(UIColor *)alertTextFieldTextColor { }]; } +- (void)setAlertTextFieldTextInsets:(UIEdgeInsets)alertTextFieldTextInsets { + _alertTextFieldTextInsets = alertTextFieldTextInsets; + [self.textFields enumerateObjectsUsingBlock:^(QMUITextField * _Nonnull textField, NSUInteger idx, BOOL * _Nonnull stop) { + textField.textInsets = alertTextFieldTextInsets; + }]; +} + +- (void)setAlertTextFieldMarginBlock:(UIEdgeInsets (^)(__kindof QMUIAlertController * _Nonnull, NSInteger))alertTextFieldMarginBlock { + _alertTextFieldMarginBlock = alertTextFieldMarginBlock; + if (self.isViewLoaded) { + [self.view setNeedsLayout]; + } +} + - (void)setMainVisualEffectView:(UIView *)mainVisualEffectView { if (!mainVisualEffectView) { // 不允许为空 @@ -524,41 +498,17 @@ - (nonnull instancetype)initWithTitle:(nullable NSString *)title message:(nullab self.preferredStyle = preferredStyle; - self.shouldRespondMaskViewTouch = preferredStyle == QMUIAlertControllerStyleActionSheet; + self.shouldRespondDimmingViewTouch = preferredStyle == QMUIAlertControllerStyleActionSheet; self.alertActions = [[NSMutableArray alloc] init]; self.alertTextFields = [[NSMutableArray alloc] init]; self.destructiveActions = [[NSMutableArray alloc] init]; - self.containerView = [[UIView alloc] init]; - - self.maskView = [[UIControl alloc] init]; - self.maskView.alpha = 0; - self.maskView.backgroundColor = UIColorMask; - [self.maskView addTarget:self action:@selector(handleMaskViewEvent:) forControlEvents:UIControlEventTouchUpInside]; - - self.scrollWrapView = [[UIView alloc] init]; - self.mainVisualEffectView = [[UIView alloc] init]; - self.cancelButtonVisualEffectView = [[UIView alloc] init]; - self.headerScrollView = [[UIScrollView alloc] init]; - self.headerScrollView.scrollsToTop = NO; - self.buttonScrollView = [[UIScrollView alloc] init]; - self.buttonScrollView.scrollsToTop = NO; - if (@available(iOS 11, *)) { - self.headerScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - self.buttonScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } - - self.extendLayer = [CALayer layer]; - self.extendLayer.hidden = !self.isExtendBottomLayout; - [self.extendLayer qmui_removeDefaultAnimations]; - self.title = title; self.message = message; - [self updateHeaderBackgrondColor]; - [self updateExtendLayerAppearance]; - + self.mainVisualEffectView = [[UIView alloc] init]; + self.cancelButtonVisualEffectView = [[UIView alloc] init]; } return self; } @@ -569,7 +519,8 @@ - (QMUIAlertControllerStyle)preferredStyle { - (void)viewDidLoad { [super viewDidLoad]; - [self.view addSubview:self.maskView]; + + [self.view addSubview:self.dimmingView]; [self.view addSubview:self.containerView]; [self.containerView addSubview:self.scrollWrapView]; [self.scrollWrapView addSubview:self.headerScrollView]; @@ -585,9 +536,10 @@ - (void)viewDidLayoutSubviews { BOOL hasMessage = (self.messageLabel.text.length > 0 && !self.messageLabel.hidden); BOOL hasTextField = self.alertTextFields.count > 0; BOOL hasCustomView = !!_customView; + BOOL shouldShowSeparatorAtTopOfButtonAtFirstLine = hasTitle || hasMessage || hasCustomView; CGFloat contentOriginY = 0; - self.maskView.frame = self.view.bounds; + self.dimmingView.frame = self.view.bounds; if (self.preferredStyle == QMUIAlertControllerStyleAlert) { @@ -613,10 +565,17 @@ - (void)viewDidLayoutSubviews { if (hasTextField) { for (int i = 0; i < self.alertTextFields.count; i++) { UITextField *textField = self.alertTextFields[i]; - textField.frame = CGRectMake(contentPaddingLeft, contentOriginY, CGRectGetWidth(self.headerScrollView.bounds) - contentPaddingLeft - contentPaddingRight, 25); - contentOriginY = CGRectGetMaxY(textField.frame) - 1; + CGRect textFieldFrame = CGRectMake(contentPaddingLeft, contentOriginY, CGRectGetWidth(self.headerScrollView.bounds) - contentPaddingLeft - contentPaddingRight, CGFLOAT_MAX); + CGSize textFieldSize = [textField sizeThatFits:textFieldFrame.size]; + textFieldFrame = CGRectSetHeight(textFieldFrame, textFieldSize.height); + UIEdgeInsets margin = UIEdgeInsetsZero; + if (self.alertTextFieldMarginBlock) { + margin = self.alertTextFieldMarginBlock(self, i); + } + textFieldFrame = CGRectMake(CGRectGetMinX(textFieldFrame) + margin.left, CGRectGetMinY(textFieldFrame) + margin.top, CGRectGetWidth(textFieldFrame) - UIEdgeInsetsGetHorizontalValue(margin), CGRectGetHeight(textFieldFrame)); + contentOriginY = CGRectGetMaxY(textFieldFrame) + margin.bottom - textField.layer.borderWidth; + textField.frame = textFieldFrame; } - contentOriginY += 16; } // 自定义view的布局 - 自动居中 if (hasCustomView) { @@ -648,16 +607,21 @@ - (void)viewDidLayoutSubviews { // 对齐系统,先 add 的在右边,后 add 的在左边 QMUIAlertAction *leftAction = newOrderActions[1]; leftAction.button.frame = CGRectMake(0, contentOriginY, CGRectGetWidth(self.buttonScrollView.bounds) / 2, self.alertButtonHeight); - leftAction.button.qmui_borderPosition = QMUIViewBorderPositionTop|QMUIViewBorderPositionRight; + leftAction.button.qmui_borderPosition = QMUIViewBorderPositionRight; QMUIAlertAction *rightAction = newOrderActions[0]; rightAction.button.frame = CGRectMake(CGRectGetMaxX(leftAction.button.frame), contentOriginY, CGRectGetWidth(self.buttonScrollView.bounds) / 2, self.alertButtonHeight); - rightAction.button.qmui_borderPosition = QMUIViewBorderPositionTop; + if (shouldShowSeparatorAtTopOfButtonAtFirstLine) { + leftAction.button.qmui_borderPosition |= QMUIViewBorderPositionTop; + rightAction.button.qmui_borderPosition = QMUIViewBorderPositionTop; + } contentOriginY = CGRectGetMaxY(leftAction.button.frame); } else { for (int i = 0; i < newOrderActions.count; i++) { QMUIAlertAction *action = newOrderActions[i]; action.button.frame = CGRectMake(0, contentOriginY, CGRectGetWidth(self.containerView.bounds), self.alertButtonHeight); - action.button.qmui_borderPosition = QMUIViewBorderPositionTop; + if (i > 0 || shouldShowSeparatorAtTopOfButtonAtFirstLine) { + action.button.qmui_borderPosition = QMUIViewBorderPositionTop; + } contentOriginY = CGRectGetMaxY(action.button.frame); } } @@ -667,7 +631,7 @@ - (void)viewDidLayoutSubviews { self.buttonScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.buttonScrollView.bounds), contentOriginY); // 容器最后布局 CGFloat contentHeight = CGRectGetHeight(self.headerScrollView.bounds) + CGRectGetHeight(self.buttonScrollView.bounds); - CGFloat screenSpaceHeight = CGRectGetHeight(self.view.bounds); + CGFloat screenSpaceHeight = CGRectGetHeight(self.view.bounds) - UIEdgeInsetsGetVerticalValue(SafeAreaInsetsConstantForDeviceWithNotch) - self.keyboardHeight; if (contentHeight > screenSpaceHeight - 20) { screenSpaceHeight -= 20; CGFloat contentH = fmin(CGRectGetHeight(self.headerScrollView.bounds), screenSpaceHeight / 2); @@ -691,8 +655,7 @@ - (void)viewDidLayoutSubviews { self.scrollWrapView.frame = CGRectMake(0, 0, CGRectGetWidth(self.scrollWrapView.bounds), contentHeight); self.mainVisualEffectView.frame = self.scrollWrapView.bounds; - CGRect containerRect = CGRectMake((CGRectGetWidth(self.view.bounds) - CGRectGetWidth(self.containerView.bounds)) / 2, (screenSpaceHeight - contentHeight - self.keyboardHeight) / 2, CGRectGetWidth(self.containerView.bounds), CGRectGetHeight(self.scrollWrapView.bounds)); - self.containerView.frame = CGRectFlatted(CGRectApplyAffineTransform(containerRect, self.containerView.transform)); + self.containerView.qmui_frameApplyTransform = CGRectMake((CGRectGetWidth(self.view.bounds) - CGRectGetWidth(self.containerView.frame)) / 2, SafeAreaInsetsConstantForDeviceWithNotch.top + (screenSpaceHeight - contentHeight) / 2, CGRectGetWidth(self.containerView.frame), CGRectGetHeight(self.scrollWrapView.bounds)); } else if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) { @@ -727,18 +690,58 @@ - (void)viewDidLayoutSubviews { contentOriginY = CGRectGetMaxY(self.headerScrollView.frame); // 按钮的布局 self.buttonScrollView.frame = CGRectMake(0, contentOriginY, CGRectGetWidth(self.containerView.bounds), 0); - contentOriginY = 0; NSArray *newOrderActions = [self orderedAlertActions:self.alertActions]; + if (self.sheetButtonColumnCount > 1) { + // 如果是多列,则为了布局,补齐 item 个数 + NSMutableArray *fixedActions = [newOrderActions mutableCopy]; + [fixedActions removeObject:self.cancelAction]; + + if (fmodf(fixedActions.count, self.sheetButtonColumnCount) != 0) { + NSInteger increment = self.sheetButtonColumnCount - fmodf(fixedActions.count, self.sheetButtonColumnCount); + for (NSInteger i = 0; i < increment; i++) { + QMUIAlertAction *action = [[QMUIAlertAction alloc] init]; + action.title = @""; + action.style = QMUIAlertActionStyleDefault; + action.handler = nil; + [self.buttonScrollView addSubview:action.button]; + [fixedActions addObject:action]; + } + + [fixedActions addObject:self.cancelAction]; + newOrderActions = [fixedActions copy]; + } + } + + CGFloat columnCount = self.sheetButtonColumnCount; + CGFloat alertActionsWidth = CGRectGetWidth(self.buttonScrollView.bounds) / columnCount; + CGFloat alertActionsLayoutX = 0; + CGFloat alertActionsLayoutY = 0; + contentOriginY = 0; if (self.alertActions.count > 0) { - contentOriginY = (hasTitle || hasMessage || hasCustomView) ? contentOriginY : contentOriginY; for (int i = 0; i < newOrderActions.count; i++) { QMUIAlertAction *action = newOrderActions[i]; if (action.style == QMUIAlertActionStyleCancel && i == newOrderActions.count - 1) { continue; } else { - action.button.frame = CGRectMake(0, contentOriginY, CGRectGetWidth(self.buttonScrollView.bounds), self.sheetButtonHeight); - action.button.qmui_borderPosition = QMUIViewBorderPositionTop; - contentOriginY = CGRectGetMaxY(action.button.frame); + BOOL isFirstLine = floor(i / columnCount) == 0; + BOOL isLastColumn = fmod(i + 1, columnCount) == 0; + BOOL shouldShowSeparatorAtTop = !isFirstLine || shouldShowSeparatorAtTopOfButtonAtFirstLine; + BOOL shouldShowSeparatorAtRight = !isLastColumn;// 单列时全都不用显示右分隔线,多列时最后一列不用显示右分隔线 + action.button.frame = CGRectMake(alertActionsLayoutX, alertActionsLayoutY, alertActionsWidth, self.sheetButtonHeight); + if (isLastColumn) { + alertActionsLayoutX = 0; + alertActionsLayoutY = CGRectGetMaxY(action.button.frame); + } else { + alertActionsLayoutX += alertActionsWidth; + } + contentOriginY = MAX(contentOriginY, CGRectGetMaxY(action.button.frame)); + + if (shouldShowSeparatorAtTop) { + action.button.qmui_borderPosition |= QMUIViewBorderPositionTop; + } + if (shouldShowSeparatorAtRight) { + action.button.qmui_borderPosition |= QMUIViewBorderPositionRight; + } } } } @@ -756,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); @@ -786,8 +789,7 @@ - (void)viewDidLayoutSubviews { contentHeight -= self.sheetContentMargin.top; } - CGRect containerRect = CGRectMake((CGRectGetWidth(self.view.bounds) - CGRectGetWidth(self.containerView.bounds)) / 2, screenSpaceHeight - contentHeight - SafeAreaInsetsConstantForDeviceWithNotch.bottom, CGRectGetWidth(self.containerView.bounds), contentHeight + (self.isExtendBottomLayout ? SafeAreaInsetsConstantForDeviceWithNotch.bottom : 0)); - self.containerView.frame = CGRectFlatted(CGRectApplyAffineTransform(containerRect, self.containerView.transform)); + 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); } @@ -843,7 +845,7 @@ - (void)customModalPresentationControllerAnimation { weakSelf.containerView.alpha = 0; weakSelf.containerView.layer.transform = CATransform3DMakeScale(1.2, 1.2, 1.0); [UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - weakSelf.maskView.alpha = 1; + weakSelf.dimmingView.alpha = 1; weakSelf.containerView.alpha = 1; weakSelf.containerView.layer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0); } completion:^(BOOL finished) { @@ -854,7 +856,7 @@ - (void)customModalPresentationControllerAnimation { } else if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) { weakSelf.containerView.layer.transform = CATransform3DMakeTranslation(0, CGRectGetHeight(weakSelf.view.bounds) - CGRectGetMinY(weakSelf.containerView.frame), 0); [UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - weakSelf.maskView.alpha = 1; + weakSelf.dimmingView.alpha = 1; weakSelf.containerView.layer.transform = CATransform3DIdentity; } completion:^(BOOL finished) { if (completion) { @@ -867,7 +869,7 @@ - (void)customModalPresentationControllerAnimation { self.modalPresentationViewController.hidingAnimation = ^(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, void(^completion)(BOOL finished)) { if (self.preferredStyle == QMUIAlertControllerStyleAlert) { [UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - weakSelf.maskView.alpha = 0; + weakSelf.dimmingView.alpha = 0; weakSelf.containerView.alpha = 0; } completion:^(BOOL finished) { weakSelf.containerView.alpha = 1; @@ -877,7 +879,7 @@ - (void)customModalPresentationControllerAnimation { }]; } else if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) { [UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - weakSelf.maskView.alpha = 0; + weakSelf.dimmingView.alpha = 0; weakSelf.containerView.layer.transform = CATransform3DMakeTranslation(0, CGRectGetHeight(weakSelf.view.bounds) - CGRectGetMinY(weakSelf.containerView.frame), 0); } completion:^(BOOL finished) { if (completion) { @@ -896,11 +898,8 @@ - (void)showWithAnimated:(BOOL)animated { if (self.alertTextFields.count > 0) { [self.alertTextFields.firstObject becomeFirstResponder]; - } else { - if (self.dismissKeyboardAutomatically && [QMUIKeyboardManager isKeyboardVisible]) { - [[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil]; - } } + if (_needsUpdateAction) { [self updateAction]; } @@ -920,13 +919,13 @@ - (void)showWithAnimated:(BOOL)animated { __weak __typeof(self)weakSelf = self; [self.modalPresentationViewController showWithAnimated:animated completion:^(BOOL finished) { - weakSelf.maskView.alpha = 1; + weakSelf.dimmingView.alpha = 1; weakSelf.willShow = NO; weakSelf.showing = YES; - if (self.isNeedsHideAfterAlertShowed) { - [self hideWithAnimated:self.isAnimatedForHideAfterAlertShowed]; - self.isNeedsHideAfterAlertShowed = NO; - self.isAnimatedForHideAfterAlertShowed = NO; + if (weakSelf.isNeedsHideAfterAlertShowed) { + [weakSelf hideWithAnimated:weakSelf.isAnimatedForHideAfterAlertShowed]; + weakSelf.isNeedsHideAfterAlertShowed = NO; + weakSelf.isAnimatedForHideAfterAlertShowed = NO; } if ([weakSelf.delegate respondsToSelector:@selector(didShowAlertController:)]) { [weakSelf.delegate didShowAlertController:weakSelf]; @@ -964,7 +963,7 @@ - (void)hideWithAnimated:(BOOL)animated completion:(void (^)(void))completion { weakSelf.modalPresentationViewController = nil; weakSelf.willShow = NO; weakSelf.showing = NO; - weakSelf.maskView.alpha = 0; + weakSelf.dimmingView.alpha = 0; if (self.preferredStyle == QMUIAlertControllerStyleAlert) { weakSelf.containerView.alpha = 0; } else { @@ -1028,6 +1027,7 @@ - (void)addTextFieldWithConfigurationHandler:(void (^)(QMUITextField *textField) textField.textColor = self.alertTextFieldTextColor; textField.autocapitalizationType = UITextAutocapitalizationTypeNone; textField.clearButtonMode = UITextFieldViewModeWhileEditing; + textField.textInsets = self.alertTextFieldTextInsets; textField.layer.borderColor = self.alertTextFieldBorderColor.CGColor; textField.layer.borderWidth = PixelOne; [self.headerScrollView addSubview:textField]; @@ -1053,7 +1053,7 @@ - (void)addCustomView:(UIView *)view { - (void)setTitle:(NSString *)title { _title = title; if (!self.titleLabel) { - self.titleLabel = [[UILabel alloc] init]; + self.titleLabel = [[QMUILabel alloc] init]; self.titleLabel.numberOfLines = 0; [self.headerScrollView addSubview:self.titleLabel]; } @@ -1079,7 +1079,7 @@ - (void)updateTitleLabel { - (void)setMessage:(NSString *)message { _message = message; if (!self.messageLabel) { - self.messageLabel = [[UILabel alloc] init]; + self.messageLabel = [[QMUILabel alloc] init]; self.messageLabel.numberOfLines = 0; [self.headerScrollView addSubview:self.messageLabel]; } @@ -1166,12 +1166,67 @@ - (void)updateAction { return [self.alertTextFields copy]; } -- (void)handleMaskViewEvent:(id)sender { - if (_shouldRespondMaskViewTouch) { +- (void)handleDimmingViewEvent:(id)sender { + if (_shouldRespondDimmingViewTouch) { [self hideWithAnimated:YES completion:NULL]; } } +#pragma mark - Getters & Setters + +- (UIControl *)dimmingView { + if (!_dimmingView) { + _dimmingView = [[UIControl alloc] init]; + _dimmingView.alpha = 0; + _dimmingView.backgroundColor = UIColorMask; + [_dimmingView addTarget:self action:@selector(handleDimmingViewEvent:) forControlEvents:UIControlEventTouchUpInside]; + } + return _dimmingView; +} + +- (UIView *)containerView { + if (!_containerView) { + _containerView = [[UIView alloc] init]; + } + return _containerView; +} + +- (UIView *)scrollWrapView { + if (!_scrollWrapView) { + _scrollWrapView = [[UIView alloc] init]; + } + return _scrollWrapView; +} + +- (UIScrollView *)headerScrollView { + if (!_headerScrollView) { + _headerScrollView = [[UIScrollView alloc] init]; + _headerScrollView.scrollsToTop = NO; + _headerScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + [self updateHeaderBackgrondColor]; + } + return _headerScrollView; +} + +- (UIScrollView *)buttonScrollView { + if (!_buttonScrollView) { + _buttonScrollView = [[UIScrollView alloc] init]; + _buttonScrollView.scrollsToTop = NO; + _buttonScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } + return _buttonScrollView; +} + +- (CALayer *)extendLayer { + if (!_extendLayer) { + _extendLayer = [CALayer layer]; + _extendLayer.hidden = !self.isExtendBottomLayout; + [_extendLayer qmui_removeDefaultAnimations]; + [self updateExtendLayerAppearance]; + } + return _extendLayer; +} + #pragma mark - - (void)didClickAlertAction:(QMUIAlertAction *)alertAction { diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.h b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.h new file mode 100644 index 00000000..46b88ad8 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.h @@ -0,0 +1,94 @@ +/** + * 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. + */ +// +// QMUIAnimationHelper.h +// WeRead +// +// Created by zhoonchen on 2018/9/3. +// + +#import +#import "QMUIEasings.h" + +@interface QMUIAnimationHelper : NSObject + +typedef NS_ENUM(NSInteger, QMUIAnimationEasings) { + QMUIAnimationEasingsLinear, + QMUIAnimationEasingsEaseInSine, + QMUIAnimationEasingsEaseOutSine, + QMUIAnimationEasingsEaseInOutSine, + QMUIAnimationEasingsEaseInQuad, + QMUIAnimationEasingsEaseOutQuad, + QMUIAnimationEasingsEaseInOutQuad, + QMUIAnimationEasingsEaseInCubic, + QMUIAnimationEasingsEaseOutCubic, + QMUIAnimationEasingsEaseInOutCubic, + QMUIAnimationEasingsEaseInQuart, + QMUIAnimationEasingsEaseOutQuart, + QMUIAnimationEasingsEaseInOutQuart, + QMUIAnimationEasingsEaseInQuint, + QMUIAnimationEasingsEaseOutQuint, + QMUIAnimationEasingsEaseInOutQuint, + QMUIAnimationEasingsEaseInExpo, + QMUIAnimationEasingsEaseOutExpo, + QMUIAnimationEasingsEaseInOutExpo, + QMUIAnimationEasingsEaseInCirc, + QMUIAnimationEasingsEaseOutCirc, + QMUIAnimationEasingsEaseInOutCirc, + QMUIAnimationEasingsEaseInBack, + QMUIAnimationEasingsEaseOutBack, + QMUIAnimationEasingsEaseInOutBack, + QMUIAnimationEasingsEaseInElastic, + QMUIAnimationEasingsEaseOutElastic, + QMUIAnimationEasingsEaseInOutElastic, + QMUIAnimationEasingsEaseInBounce, + QMUIAnimationEasingsEaseOutBounce, + QMUIAnimationEasingsEaseInOutBounce, + QMUIAnimationEasingsSpring, // 自定义任意弹簧曲线 + QMUIAnimationEasingsSpringKeyboard // 系统键盘动画曲线 +}; + +/** + * 动画插值器 + * 根据给定的 easing 曲线,计算出初始值和结束值在当前的时间 time 对应的值。value 目前现在支持 NSNumber、UIColor 以及 NSValue 类型的 CGPoint、CGSize、CGRect、CGAffineTransform、UIEdgeInsets + * @param fromValue 初始值 + * @param toValue 结束值 + * @param time 当前帧时间 + * @param easing 曲线,见`QMUIAnimationEasings` + */ ++ (id)interpolateFromValue:(id)fromValue + toValue:(id)toValue + time:(CGFloat)time + easing:(QMUIAnimationEasings)easing; +/** + * 动画插值器,支持弹簧参数 + * mass|damping|stiffness|initialVelocity 仅在 QMUIAnimationEasingsSpring 的时候才生效 + */ ++ (id)interpolateSpringFromValue:(id)fromValue + toValue:(id)toValue + time:(CGFloat)time + mass:(CGFloat)mass + damping:(CGFloat)damping + stiffness:(CGFloat)stiffness + initialVelocity:(CGFloat)initialVelocity + easing:(QMUIAnimationEasings)easing; + +/** + 类似系统 UIScrollView 在拖拽到内容尽头时会越拖越难拖的效果。 + @param fromValue 初始值,一般为 0。 + @param toValue 目标值,也即你希望拖拽到的极限距离。 + @param time 当前拖拽距离相对于极限距离的百分比,0 表示在 fromValue,1 表示拖拽到与极限距离相同的大小,大于1表示拖拽得比极限距离还远。 + @param coeff 取值范围-1~+∞。值越大,拖拽的初期越容易拖动。例如 0.1 表示从头到尾都很难拖动,9表示一开始稍微拖一下就可以动很长距离(也可以理解为只需要很短的拖拽动作就可以很快接近极限距离)。-1 表示用默认的 0.55,也即系统的 UIScrollView 的系数。 + @return 返回当前 time 对应的移动距离,返回值大于等于 fromValue,小于 toValue(只会无限接近,不可能等于)。 + */ ++ (CGFloat)bounceFromValue:(CGFloat)fromValue + toValue:(CGFloat)toValue + time:(CGFloat)time + coeff:(CGFloat)coeff; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.m b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.m new file mode 100644 index 00000000..a1fd0e31 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.m @@ -0,0 +1,239 @@ +/** + * 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. + */ +// +// QMUIAnimationHelper.m +// WeRead +// +// Created by zhoonchen on 2018/9/3. +// + +#import "QMUIAnimationHelper.h" +#import "QMUICore.h" + +#define SpringDefaultMass 1.0 +#define SpringDefaultDamping 18.0 +#define SpringDefaultStiffness 82.0 +#define SpringDefaultInitialVelocity 0.0 + +@implementation QMUIAnimationHelper + ++ (CGFloat)bounceFromValue:(CGFloat)fromValue toValue:(CGFloat)toValue time:(CGFloat)time coeff:(CGFloat)coeff { + // 以下算法来源于社区: + // How UIScrollView works: https://medium.com/@esskeetit/how-uiscrollview-works-e418adc47060 + // Grant Paul's Twitter: https://twitter.com/chpwn/status/285540192096497664 + coeff = coeff == -1 ? 0.55 : coeff;// 0.55 为系统 UIScrollView 的默认系数,这里我们也将其作为我们的默认系数 + CGFloat d = toValue; + CGFloat x = (d - fromValue) * time; + CGFloat result = fromValue + d + 1.0 - (1.0 / (coeff * x / d + 1)) * d; +// NSLog(@"[%.2f-%.2f], coeff = %.2f, x = %.2f, result = %.2f", fromValue, d, coeff, x, result); + return result; +} + ++ (id)interpolateFromValue:(id)fromValue + toValue:(id)toValue + time:(CGFloat)time + easing:(QMUIAnimationEasings)easing { + return [self interpolateSpringFromValue:fromValue toValue:toValue time:time mass:SpringDefaultMass damping:SpringDefaultDamping stiffness:SpringDefaultStiffness initialVelocity:SpringDefaultInitialVelocity easing:easing]; +} + +/* + * 插值器,遇到新的类型再添加 + */ ++ (id)interpolateSpringFromValue:(id)fromValue + toValue:(id)toValue + time:(CGFloat)time + mass:(CGFloat)mass + damping:(CGFloat)damping + stiffness:(CGFloat)stiffness + initialVelocity:(CGFloat)initialVelocity + easing:(QMUIAnimationEasings)easing { + + if ([fromValue isKindOfClass:[NSNumber class]]) { // NSNumber + CGFloat from = [fromValue floatValue]; + CGFloat to = [toValue floatValue]; + CGFloat result = interpolateSpring(from, to, time, easing, mass, damping, stiffness, initialVelocity); + return [NSNumber numberWithFloat:result]; + } + + else if ([fromValue isKindOfClass:[UIColor class]]) { // UIColor + UIColor *from = (UIColor *)fromValue; + UIColor *to = (UIColor *)toValue; + CGFloat fromRed, toRed, curRed = 0; + CGFloat fromGreen, toGreen, curGreen = 0; + CGFloat fromBlue, toBlue, curBlue = 0; + CGFloat fromAlpha, toAlpha, curAlpha = 0; + [from getRed:&fromRed green:&fromGreen blue:&fromBlue alpha:&fromAlpha]; + [to getRed:&toRed green:&toGreen blue:&toBlue alpha:&toAlpha]; + curRed = interpolateSpring(fromRed, toRed, time, easing, mass, damping, stiffness, initialVelocity); + curGreen = interpolateSpring(fromGreen, toGreen, time, easing, mass, damping, stiffness, initialVelocity); + curBlue = interpolateSpring(fromBlue, toBlue, time, easing, mass, damping, stiffness, initialVelocity); + curAlpha = interpolateSpring(fromAlpha, toAlpha, time, easing, mass, damping, stiffness, initialVelocity); + UIColor *result = [UIColor colorWithRed:curRed green:curGreen blue:curBlue alpha:curAlpha]; + return result; + } + + else if ([fromValue isKindOfClass:[NSValue class]]) { // NSValue + const char *type = [(NSValue *)fromValue objCType]; + if (strcmp(type, @encode(CGPoint)) == 0) { + CGPoint from = [fromValue CGPointValue]; + CGPoint to = [toValue CGPointValue]; + CGPoint result = CGPointMake(interpolateSpring(from.x, to.x, time, easing, mass, damping, stiffness, initialVelocity), interpolateSpring(from.y, to.y, time, easing, mass, damping, stiffness, initialVelocity)); + return [NSValue valueWithCGPoint:result]; + } + else if (strcmp(type, @encode(CGSize)) == 0) { + CGSize from = [fromValue CGSizeValue]; + CGSize to = [toValue CGSizeValue]; + CGSize result = CGSizeMake(interpolateSpring(from.width, to.width, time, easing, mass, damping, stiffness, initialVelocity), interpolateSpring(from.height, to.height, time, easing, mass, damping, stiffness, initialVelocity)); + return [NSValue valueWithCGSize:result]; + } + else if (strcmp(type, @encode(CGRect)) == 0) { + CGRect from = [fromValue CGRectValue]; + CGRect to = [toValue CGRectValue]; + CGRect result = CGRectMake(interpolateSpring(from.origin.x, to.origin.x, time, easing, mass, damping, stiffness, initialVelocity), interpolateSpring(from.origin.y, to.origin.y, time, easing, mass, damping, stiffness, initialVelocity), interpolateSpring(from.size.width, to.size.width, time, easing, mass, damping, stiffness, initialVelocity), interpolateSpring(from.size.height, to.size.height, time, easing, mass, damping, stiffness, initialVelocity)); + return [NSValue valueWithCGRect:result]; + } + else if (strcmp(type, @encode(CGAffineTransform)) == 0) { + CGAffineTransform from = [fromValue CGAffineTransformValue]; + CGAffineTransform to = [toValue CGAffineTransformValue]; + CGAffineTransform result = CGAffineTransformIdentity; + result.a = interpolateSpring(from.a, to.a, time, easing, mass, damping, stiffness, initialVelocity); + result.b = interpolateSpring(from.b, to.b, time, easing, mass, damping, stiffness, initialVelocity); + result.c = interpolateSpring(from.c, to.c, time, easing, mass, damping, stiffness, initialVelocity); + result.d = interpolateSpring(from.d, to.d, time, easing, mass, damping, stiffness, initialVelocity); + result.tx = interpolateSpring(from.tx, to.tx, time, easing, mass, damping, stiffness, initialVelocity); + result.ty = interpolateSpring(from.ty, to.ty, time, easing, mass, damping, stiffness, initialVelocity); + return [NSValue valueWithCGAffineTransform:result]; + } + else if (strcmp(type, @encode(UIEdgeInsets)) == 0) { + UIEdgeInsets from = [fromValue UIEdgeInsetsValue]; + UIEdgeInsets to = [toValue UIEdgeInsetsValue]; + UIEdgeInsets result = UIEdgeInsetsZero; + result.top = interpolateSpring(from.top, to.top, time, easing, mass, damping, stiffness, initialVelocity); + result.left = interpolateSpring(from.left, to.left, time, easing, mass, damping, stiffness, initialVelocity); + result.bottom = interpolateSpring(from.bottom, to.bottom, time, easing, mass, damping, stiffness, initialVelocity); + result.right = interpolateSpring(from.right, to.right, time, easing, mass, damping, stiffness, initialVelocity); + return [NSValue valueWithUIEdgeInsets:result]; + } + } + + return (time < 0.5) ? fromValue: toValue; +} + +CGFloat interpolate(CGFloat from, CGFloat to, CGFloat time, QMUIAnimationEasings easing) { + return interpolateSpring(from, to, time, easing, SpringDefaultMass, SpringDefaultDamping, SpringDefaultStiffness, SpringDefaultInitialVelocity); +} + +CGFloat interpolateSpring(CGFloat from, CGFloat to, CGFloat time, QMUIAnimationEasings easing, CGFloat springMass, CGFloat springDamping, CGFloat springStiffness, CGFloat springInitialVelocity) { + switch (easing) { + case QMUIAnimationEasingsLinear: + time = QMUI_Linear(time); + break; + case QMUIAnimationEasingsEaseInSine: + time = QMUI_EaseInSine(time); + break; + case QMUIAnimationEasingsEaseOutSine: + time = QMUI_EaseOutSine(time); + break; + case QMUIAnimationEasingsEaseInOutSine: + time = QMUI_EaseInOutSine(time); + break; + case QMUIAnimationEasingsEaseInQuad: + time = QMUI_EaseInQuad(time); + break; + case QMUIAnimationEasingsEaseOutQuad: + time = QMUI_EaseOutQuad(time); + break; + case QMUIAnimationEasingsEaseInOutQuad: + time = QMUI_EaseInOutQuad(time); + break; + case QMUIAnimationEasingsEaseInCubic: + time = QMUI_EaseInCubic(time); + break; + case QMUIAnimationEasingsEaseOutCubic: + time = QMUI_EaseOutCubic(time); + break; + case QMUIAnimationEasingsEaseInOutCubic: + time = QMUI_EaseInOutCubic(time); + break; + case QMUIAnimationEasingsEaseInQuart: + time = QMUI_EaseInQuart(time); + break; + case QMUIAnimationEasingsEaseOutQuart: + time = QMUI_EaseOutQuart(time); + break; + case QMUIAnimationEasingsEaseInOutQuart: + time = QMUI_EaseInOutQuart(time); + break; + case QMUIAnimationEasingsEaseInQuint: + time = QMUI_EaseInQuint(time); + break; + case QMUIAnimationEasingsEaseOutQuint: + time = QMUI_EaseOutQuint(time); + break; + case QMUIAnimationEasingsEaseInOutQuint: + time = QMUI_EaseInOutQuint(time); + break; + case QMUIAnimationEasingsEaseInExpo: + time = QMUI_EaseInExpo(time); + break; + case QMUIAnimationEasingsEaseOutExpo: + time = QMUI_EaseOutExpo(time); + break; + case QMUIAnimationEasingsEaseInOutExpo: + time = QMUI_EaseInOutExpo(time); + break; + case QMUIAnimationEasingsEaseInCirc: + time = QMUI_EaseInCirc(time); + break; + case QMUIAnimationEasingsEaseOutCirc: + time = QMUI_EaseOutCirc(time); + break; + case QMUIAnimationEasingsEaseInOutCirc: + time = QMUI_EaseInOutCirc(time); + break; + case QMUIAnimationEasingsEaseInBack: + time = QMUI_EaseInBack(time); + break; + case QMUIAnimationEasingsEaseOutBack: + time = QMUI_EaseOutBack(time); + break; + case QMUIAnimationEasingsEaseInOutBack: + time = QMUI_EaseInOutBack(time); + break; + case QMUIAnimationEasingsEaseInElastic: + time = QMUI_EaseInElastic(time); + break; + case QMUIAnimationEasingsEaseOutElastic: + time = QMUI_EaseOutElastic(time); + break; + case QMUIAnimationEasingsEaseInOutElastic: + time = QMUI_EaseInOutElastic(time); + break; + case QMUIAnimationEasingsEaseInBounce: + time = QMUI_EaseInBounce(time); + break; + case QMUIAnimationEasingsEaseOutBounce: + time = QMUI_EaseOutBounce(time); + break; + case QMUIAnimationEasingsEaseInOutBounce: + time = QMUI_EaseInOutBounce(time); + break; + case QMUIAnimationEasingsSpring: + time = QMUI_EaseSpring(time, springMass, springDamping, springStiffness, springInitialVelocity); + break; + case QMUIAnimationEasingsSpringKeyboard: + time = QMUI_EaseSpring(time, SpringDefaultMass, SpringDefaultDamping, SpringDefaultStiffness, SpringDefaultInitialVelocity); + break; + default: + time = QMUI_Linear(time); + break; + } + return (to - from) * time + from; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.h b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.h new file mode 100644 index 00000000..6addd7c2 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.h @@ -0,0 +1,140 @@ +/** + * 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. + */ +// +// QMUIDisplayLinkAnimation.h +// WeRead +// +// Created by zhoonchen on 2018/9/3. +// + +#import +#import "QMUIAnimationHelper.h" + +#define SpringAnimationDefaultDuration 0.5 + + +/* + * 通过 CADisplayLink 来做动画,接口尽可能模拟 CAAnimation。有如下好处: + * 1、跟随系统刷新频率 + * 2、因为使用了 CADisplayLink,所以理论上所有数据都可以做动画,而不局限于 CALayer 的 UI 属性 + * 3、避免 CAAnimation 有时候系统会自动暂停(例如 app 退到后台再进来,或者切到其他界面再回来) + * 4、更多动画曲线可以选择,包括弹簧动画以及类似系统的键盘曲线动画。 + * @warning: ⚠️⚠️⚠️ 当动画是无限循环的时候,需要在某个时机去 stop 动画(例如 dealloc 里面),否则 `QMUIDisplayLinkAnimation` 对象永远都不会释放,对应的 CADisplayLink 在后台都会被调用。 + */ + +@interface QMUIDisplayLinkAnimation : NSObject + +@property(nonatomic, strong, readonly) CADisplayLink *displayLink; + +@property(nonatomic, strong) id fromValue; +@property(nonatomic, strong) id toValue; + +/// 动画时间 +@property(nonatomic, assign) NSTimeInterval duration; + +/// 动画曲线 +@property(nonatomic, assign) QMUIAnimationEasings easing; + +/// 是否需要重复,如果设置为YES,那么会无限重复动画,默认NO +/// TODO: 目前功能上不支持小数点的循环次数,例如 0.5 1.5 +@property(nonatomic, assign) BOOL repeat; + +/// 延迟开始动画 +@property(nonatomic, assign) NSTimeInterval beginTime; + +/// 只有设置了repeat之后这个值才有用 +@property(nonatomic, assign) float repeatCount; + +/// 只有设置了repeat之后这个值才有用。如果YES,则往前做动画之后自动往后做动画,默认NO +@property(nonatomic, assign) BOOL autoreverses; + +/// 做动画的block,适用于只有一个属性需要做动画,curValue是经过计算后当前帧的值 +@property(nonatomic, copy) void (^animation)(id curValue); + +/// 做动画的block,适用于多个属性做动画,需要在block里面自己计算当前帧的所有属性的值 +@property(nonatomic, copy) void (^animations)(QMUIDisplayLinkAnimation *animation, CGFloat curTime); + +- (instancetype)initWithDuration:(NSTimeInterval)duration + easing:(QMUIAnimationEasings)easing + fromValue:(id)fromValue + toValue:(id)toValue + animation:(void (^)(id curValue))animation; + +- (instancetype)initWithDuration:(NSTimeInterval)duration + easing:(QMUIAnimationEasings)easing + animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations; + +/// 开始动画,无论是第一次做动画或者暂停之后再重新做动画,都调用这个方法 +- (void)startAnimation; + +/// 停止动画,CADisplayLink 对象会被移出 +- (void)stopAnimation; + +/// 即将开始做动画 +@property(nonatomic, copy) void (^willStartAnimation)(void); + +/// 动画结束 +@property(nonatomic, copy) void (^didStopAnimation)(void); + +@end + + +@interface QMUIDisplayLinkAnimation (ConvenienceClassMethod) + +/* + * 这些类方法在动画执行之后会自动销毁 QMUIDisplayLinkAnimation 对象,因为此时没有人持有这个对象(有个坑就是如果这个动画是无限循环的,那么就一直无法销毁,需要业务手动销毁)。如果想要持有对象以便在后续操作,可以把返回值保存到其他属性里面。 + * `createdBlock` 是 animation 创建之后,开始动画之前的回调,一般用来设置 animation 属性,比如是否重复动画以及重复的次数。 + * `didStopBlock` 是动画结束之后的回调。 + * @warning: block 中的代码记得使用弱引用,以免内存泄漏。 + */ + ++ (instancetype)springAnimateWithFromValue:(id)fromValue + toValue:(id)toValue + animation:(void (^)(id curValue))animation + createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock; + ++ (instancetype)animateWithDuration:(NSTimeInterval)duration + easing:(QMUIAnimationEasings)easing + fromValue:(id)fromValue + toValue:(id)toValue + animation:(void (^)(id curValue))animation; + ++ (instancetype)animateWithDuration:(NSTimeInterval)duration + easing:(QMUIAnimationEasings)easing + fromValue:(id)fromValue + toValue:(id)toValue + animation:(void (^)(id curValue))animation + createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock; + ++ (instancetype)animateWithDuration:(NSTimeInterval)duration + easing:(QMUIAnimationEasings)easing + fromValue:(id)fromValue + toValue:(id)toValue + animation:(void (^)(id curValue))animation + createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock + didStopBlock:(void (^)(QMUIDisplayLinkAnimation *animation))didStopBlock; + ++ (instancetype)springAnimateWithAnimations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations + createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock; + ++ (instancetype)animateWithDuration:(NSTimeInterval)duration + easing:(QMUIAnimationEasings)easing + animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations; + ++ (instancetype)animateWithDuration:(NSTimeInterval)duration + easing:(QMUIAnimationEasings)easing + animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations + createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock; + ++ (instancetype)animateWithDuration:(NSTimeInterval)duration + easing:(QMUIAnimationEasings)easing + animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations + createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock + didStopBlock:(void (^)(QMUIDisplayLinkAnimation *animation))didStopBlock; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.m b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.m new file mode 100644 index 00000000..bc3c42b6 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.m @@ -0,0 +1,290 @@ +/** + * 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. + */ +// +// QMUIDisplayLinkAnimation.m +// WeRead +// +// Created by zhoonchen on 2018/9/3. +// + +#import "QMUIDisplayLinkAnimation.h" +#import "QMUICore.h" + +@interface QMUIDisplayLinkAnimation () + +@property(nonatomic, strong, readwrite) CADisplayLink *displayLink; + +@property(nonatomic, assign) NSTimeInterval timeOffset; +@property(nonatomic, assign) NSInteger curRepeatCount; +@property(nonatomic, assign) BOOL isReversing; + +@end + +@implementation QMUIDisplayLinkAnimation + +- (instancetype)init { + self = [super init]; + if (self) { + self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)]; + self.fromValue = nil; + self.toValue = nil; + self.duration = 0; + self.repeatCount = 0; + self.easing = QMUIAnimationEasingsLinear; + self.timeOffset = 0; + self.animation = nil; + } + return self; +} + +- (instancetype)initWithDuration:(CFTimeInterval)duration + easing:(QMUIAnimationEasings)easing + fromValue:(id)fromValue + toValue:(id)toValue + animation:(void (^)(id curValue))animation { + if (self = [self init]) { + self.duration = duration; + self.easing = easing; + self.fromValue = fromValue; + self.toValue = toValue; + self.animation = animation; + } + return self; +} + +- (instancetype)initWithDuration:(CFTimeInterval)duration + easing:(QMUIAnimationEasings)easing + animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations { + if (self = [self init]) { + self.duration = duration; + self.easing = easing; + self.animations = animations; + } + return self; +} + +- (void)dealloc { + [_displayLink invalidate]; + _displayLink = nil; +} + +- (void)startAnimation { + if (!self.displayLink) { + return; + } + if (self.displayLink.paused) { + self.displayLink.paused = NO; + return; + } + if (self.beginTime > 0) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.beginTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (self.displayLink) { + if (self.willStartAnimation) { + self.willStartAnimation(); + } + [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } + }); + } else { + if (self.willStartAnimation) { + self.willStartAnimation(); + } + [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } +} + +- (void)stopAnimation { + [self.displayLink invalidate]; + self.displayLink = nil; + if (self.didStopAnimation) { + self.didStopAnimation(); + } +} + +- (void)handleDisplayLink:(CADisplayLink *)displayLink { + if (!self.animation && !self.animations) { + return; + } + NSTimeInterval oneFrame = 1.0 / [self preferredFramesPerSecond]; + if (self.autoreverses && self.isReversing) { + self.timeOffset = MAX(self.timeOffset - oneFrame, 0); + } else { + self.timeOffset = MIN(self.timeOffset + oneFrame, self.duration); + } + CGFloat time = self.timeOffset / self.duration; + if (self.animations) { + self.animations(self, time); + } else if (self.animation) { + id curValue = [QMUIAnimationHelper interpolateFromValue:self.fromValue toValue:self.toValue time:time easing:self.easing]; + self.animation(curValue); + } + if (self.timeOffset >= self.duration) { + [self beginToDecrease]; + } else if (self.timeOffset <= 0) { + [self beginToIncrease]; + } +} + +- (void)beginToIncrease { + if (self.repeat && self.repeatCount > 0) { + self.curRepeatCount++; + } + if (self.autoreverses) { + self.isReversing = NO; + } + if (self.curRepeatCount >= self.repeatCount) { + [self stopAnimation]; + } +} + +- (void)beginToDecrease { + if (self.repeat && self.repeatCount > 0) { + self.curRepeatCount++; + } + if (self.repeat) { + if (self.autoreverses) { + self.isReversing = YES; + } else { + self.timeOffset = 0; + } + if (self.curRepeatCount >= self.repeatCount) { + [self stopAnimation]; + } + } else { + [self stopAnimation]; + } +} + +- (NSInteger)preferredFramesPerSecond { + if (self.displayLink.preferredFramesPerSecond == 0) { + // 不能写死60,而要拿当前设备支持的最大帧率来计算。根据 CADisplayLink 的官方文档,如果返回一个超过当前设备实际帧率的数字,实际依然会用设备实际帧率来计算,所以不用担心设备降频导致帧率降低后动画时长是否有问题。 + return UIScreen.mainScreen.maximumFramesPerSecond; + } + return self.displayLink.preferredFramesPerSecond; +} + +@end + + +@implementation QMUIDisplayLinkAnimation (ConvenienceClassMethod) + ++ (instancetype)springAnimateWithFromValue:(id)fromValue + toValue:(id)toValue + animation:(void (^)(id curValue))animation + createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock { + return [self animateWithDuration:SpringAnimationDefaultDuration + easing:QMUIAnimationEasingsSpringKeyboard + fromValue:fromValue + toValue:toValue + animation:animation + createdBlock:createdBlock]; +} + ++ (instancetype)animateWithDuration:(NSTimeInterval)duration + easing:(QMUIAnimationEasings)easing + fromValue:(id)fromValue + toValue:(id)toValue + animation:(void (^)(id curValue))animation { + return [self animateWithDuration:duration + easing:easing + fromValue:fromValue + toValue:toValue + animation:animation + createdBlock:nil]; +} + ++ (instancetype)animateWithDuration:(NSTimeInterval)duration + easing:(QMUIAnimationEasings)easing + fromValue:(id)fromValue + toValue:(id)toValue + animation:(void (^)(id curValue))animation + createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock { + return [self animateWithDuration:duration + easing:easing + fromValue:fromValue + toValue:toValue + animation:animation + createdBlock:createdBlock + didStopBlock:nil]; +} + ++ (instancetype)animateWithDuration:(NSTimeInterval)duration + easing:(QMUIAnimationEasings)easing + fromValue:(id)fromValue + toValue:(id)toValue + animation:(void (^)(id curValue))animation + createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock + didStopBlock:(void (^)(QMUIDisplayLinkAnimation *animation))didStopBlock { + QMUIDisplayLinkAnimation *displayLinkAnimation = [[self alloc] initWithDuration:duration + easing:easing + fromValue:fromValue + toValue:toValue + animation:animation]; + if (createdBlock) { + createdBlock(displayLinkAnimation); + } + __weak QMUIDisplayLinkAnimation *weakDisplayLinkAnimation = displayLinkAnimation; + displayLinkAnimation.didStopAnimation = ^{ + if (didStopBlock) { + didStopBlock(weakDisplayLinkAnimation); + } + }; + [displayLinkAnimation startAnimation]; + return displayLinkAnimation; +} + ++ (instancetype)springAnimateWithAnimations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations + createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock { + return [self animateWithDuration:SpringAnimationDefaultDuration + easing:QMUIAnimationEasingsSpringKeyboard + animations:animations + createdBlock:createdBlock]; +} + ++ (instancetype)animateWithDuration:(NSTimeInterval)duration + easing:(QMUIAnimationEasings)easing + animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations { + return [self animateWithDuration:duration + easing:easing + animations:animations + createdBlock:nil]; +} + ++ (instancetype)animateWithDuration:(NSTimeInterval)duration + easing:(QMUIAnimationEasings)easing + animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations + createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock { + return [self animateWithDuration:duration + easing:easing + animations:animations + createdBlock:createdBlock + didStopBlock:nil]; +} + ++ (instancetype)animateWithDuration:(NSTimeInterval)duration + easing:(QMUIAnimationEasings)easing + animations:(void (^)(QMUIDisplayLinkAnimation *animation, CGFloat curTime))animations + createdBlock:(void (^)(QMUIDisplayLinkAnimation *animation))createdBlock + didStopBlock:(void (^)(QMUIDisplayLinkAnimation *animation))didStopBlock { + QMUIDisplayLinkAnimation *displayLinkAnimation = [[self alloc] initWithDuration:duration + easing:easing + animations:animations]; + if (createdBlock) { + createdBlock(displayLinkAnimation); + } + __weak QMUIDisplayLinkAnimation *weakDisplayLinkAnimation = displayLinkAnimation; + displayLinkAnimation.didStopAnimation = ^{ + if (didStopBlock) { + didStopBlock(weakDisplayLinkAnimation); + } + }; + [displayLinkAnimation startAnimation]; + return displayLinkAnimation; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIEasings.h b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIEasings.h new file mode 100644 index 00000000..938e40cf --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIAnimation/QMUIEasings.h @@ -0,0 +1,233 @@ +/** + * 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. + */ +// +// QMUIEasings.h +// WeRead +// +// Created by zhoonchen on 2018/9/3. +// + +#import + +/// https://easings.net +/// http://cubic-bezier.com + +CG_INLINE CGFloat +QMUI_Linear(CGFloat t) { + return t; +} + +CG_INLINE CGFloat +QMUI_EaseInSine(CGFloat t) { + return 1 - cos(t * M_PI_2); +} + +CG_INLINE CGFloat +QMUI_EaseOutSine(CGFloat t) { + return sin(t * M_PI_2); +} + +CG_INLINE CGFloat +QMUI_EaseInOutSine(CGFloat t) { + return - (cos(M_PI * t) - 1) / 2; +} + +CG_INLINE CGFloat +QMUI_EaseInQuad(CGFloat t) { + return pow(t, 2); +} + +CG_INLINE CGFloat +QMUI_EaseOutQuad(CGFloat t) { + return 1 - pow(1 - t, 2); +} + +CG_INLINE CGFloat +QMUI_EaseInOutQuad(CGFloat t) { + return t < 0.5 ? (2 * pow(t, 2)) : (1 - pow(-2 * t + 2, 2) / 2); +} + +CG_INLINE CGFloat +QMUI_EaseInCubic(CGFloat t) { + return pow(t, 3); +} + +CG_INLINE CGFloat +QMUI_EaseOutCubic(CGFloat t) { + return 1 - pow(1 - t, 3); +} + +CG_INLINE CGFloat +QMUI_EaseInOutCubic(CGFloat t) { + return t < 0.5 ? (4 * pow(t, 3)) : (1 - pow(-2 * t + 2, 3) / 2); +} + +CG_INLINE CGFloat +QMUI_EaseInQuart(CGFloat t) { + return pow(t, 4); +} + +CG_INLINE CGFloat +QMUI_EaseOutQuart(CGFloat t) { + return 1 - pow(1 - t, 4); +} + +CG_INLINE CGFloat +QMUI_EaseInOutQuart(CGFloat t) { + return t < 0.5 ? (8 * pow(t, 4)) : (1 - pow(-2 * t + 2, 4) / 2); +} + +CG_INLINE CGFloat +QMUI_EaseInQuint(CGFloat t) { + return pow(t, 5); +} + +CG_INLINE CGFloat +QMUI_EaseOutQuint(CGFloat t) { + return 1 - pow(1 - t, 5); +} + +CG_INLINE CGFloat +QMUI_EaseInOutQuint(CGFloat t) { + return t < 0.5 ? (16 * pow(t, 5)) : (1 - pow(-2 * t + 2, 5) / 2); +} + +CG_INLINE CGFloat +QMUI_EaseInExpo(CGFloat t) { + return t == 0 ? 0 : pow(2, 10 * t - 10); +} + +CG_INLINE CGFloat +QMUI_EaseOutExpo(CGFloat t) { + return t == 1 ? 1 : 1 - pow(2, -10 * t); +} + +CG_INLINE CGFloat +QMUI_EaseInOutExpo(CGFloat t) { + return t == 0 ? 0 : t == 1 ? 1 : t < 0.5 ? pow(2, 20 * t - 10 ) / 2 : (2 - pow(2, -20 * t + 10 )) / 2; +} + +CG_INLINE CGFloat +QMUI_EaseInCirc(CGFloat t) { + return 1 - sqrt(1 - pow(t, 2)); +} + +CG_INLINE CGFloat +QMUI_EaseOutCirc(CGFloat t) { + return sqrt(1 - pow(t - 1, 2)); +} + +CG_INLINE CGFloat +QMUI_EaseInOutCirc(CGFloat t) { + return t < 0.5 ? (1 - sqrt(1 - pow(2 * t, 2))) / 2 : (sqrt(1 - pow(-2 * t + 2, 2)) + 1) / 2; +} + +CG_INLINE CGFloat +QMUI_EaseInBack(CGFloat t) { + return pow(t, 3) - t * sin(t * M_PI); +} + +CG_INLINE CGFloat +QMUI_EaseOutBack(CGFloat t) { + CGFloat f = (1 - t); + return 1 - (pow(f, 3) - f * sin(f * M_PI)); +} + +CG_INLINE CGFloat +QMUI_EaseInOutBack(CGFloat t) { + if (t < 0.5) { + CGFloat f = 2 * t; + return 0.5 * (pow(f, 3) - f * sin(f * M_PI)); + } else { + CGFloat f = (1 - (2 * t - 1)); + return 0.5 * (1 - (pow(f, 3) - f * sin(f * M_PI))) + 0.5; + } +} + +CG_INLINE CGFloat +QMUI_EaseInElastic(CGFloat t) { + return sin(13 * M_PI_2 * t) * pow(2, 10 * (t - 1)); +} + +CG_INLINE CGFloat +QMUI_EaseOutElastic(CGFloat t) { + return sin(-13 * M_PI_2 * (t + 1)) * pow(2, -10 * t) + 1; +} + +CG_INLINE CGFloat +QMUI_EaseInOutElastic(CGFloat t) { + if (t < 0.5) { + return 0.5 * sin(13 * M_PI_2 * (2 * t)) * pow(2, 10 * ((2 * t) - 1)); + } else { + return 0.5 * (sin(-13 * M_PI_2 * ((2 * t - 1) + 1)) * pow(2, -10 * (2 * t - 1)) + 2); + } +} + +CG_INLINE CGFloat +QMUI_EaseOutBounce(CGFloat t) { + if (t < 4.0 / 11.0) { + return (121.0 * t * t) / 16.0; + } else if (t < 8.0 / 11.0) { + return (363.0 / 40.0 * t * t) - (99.0 / 10.0 * t) + 17.0 / 5.0; + } else if(t < 9.0 / 10.0) { + return (4356.0 / 361.0 * t * t) - (35442.0 / 1805.0 * t) + 16061.0 / 1805.0; + } else { + return (54.0 / 5.0 * t * t) - (513.0 / 25.0 * t) + 268.0 / 25.0; + } +} + +CG_INLINE CGFloat +QMUI_EaseInBounce(CGFloat t) { + return 1 - QMUI_EaseOutBounce(1 - t); +} + +CG_INLINE CGFloat +QMUI_EaseInOutBounce(CGFloat t) { + if (t < 0.5) { + return 0.5 * QMUI_EaseInBounce(t * 2); + } else { + return 0.5 * QMUI_EaseOutBounce(t * 2 - 1) + 0.5; + } +} + +CG_INLINE CGFloat +QMUI_EaseSpring(CGFloat t, CGFloat mass, CGFloat damping, CGFloat stiffness, CGFloat initialVelocity) { + + // https://webkit.org/demos/spring/spring.js + // https://webkit.org/demos/spring + + CGFloat m_w0 = sqrt(stiffness / mass); + CGFloat m_zeta = damping / (2 * sqrt(stiffness * mass)); + + CGFloat m_wd = 0; + CGFloat m_A = 0; + CGFloat m_B = 0; + + if (m_zeta < 1) { + // Under-damped. + m_wd = m_w0 * sqrt(1 - m_zeta * m_zeta); + m_A = 1; + m_B = (m_zeta * m_w0 + -initialVelocity) / m_wd; + } else { + // Critically damped (ignoring over-damped case for now). + m_wd = 0; + m_A = 1; + m_B = -initialVelocity + m_w0; + } + + if (m_zeta < 1) { + // Under-damped + t = exp(-t * m_zeta * m_w0) * (m_A * cos(m_wd * t) + m_B * sin(m_wd * t)); + } else { + // Critically damped (ignoring over-damped case for now). + t = (m_A + m_B * t) * exp(-t * m_w0); + } + + // Map range from [1..0] to [0..1]. + return 1 - t; +} diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAppearance.h b/QMUI/QMUIKit/QMUIComponents/QMUIAppearance.h new file mode 100644 index 00000000..f4f96b09 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIAppearance.h @@ -0,0 +1,52 @@ +/** + * 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. + */ +// +// QMUIAppearance.h +// QMUIKit +// +// Created by MoLice on 2020/3/25. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** +UIKit 仅提供了对 UIView 默认的 UIAppearance 支持,如果你是一个继承自 NSObject 的对象,想要使用 UIAppearance 能力,按 UIKit 公开的 API 是无法实现的,而 QMUIAppearance 对这种场景提供了支持。 + +使用方法(可参考 QMUIAlertController): + +1. 为目标类增加方法 +(instancetype)appearance; 方法,返回值类型使用 instancetype 是为了保证 Xcode 能正确进行代码提示,命名无限制,用 appearance 只是为了统一。 + +2. 为目标类支持 appearance 的属性、方法添加 UI_APPEARANCE_SELECTOR 标记,注意对于方法只有符合特定命名格式才支持,具体请查看 UIAppearance.h 顶部对宏 UI_APPEARANCE_SELECTOR 的注释。 + +3. 在 +appearance 方法里通过 +[QMUIAppearance appearanceForClass:self] 得到 appearance 对象并返回。 + +4. 在恰当的时机为目标类的 appearance 赋初始值,QMUI 通常在类的 +initialize 方法里赋值。如果你支持 UI_APPEARANCE_SELECTOR 的属性默认值都为 nil,也可以忽略这一步。 + +5. 在类初始化实例的时候(例如 init 方法里)调用 -qmui_applyQMUIAppearance 为实例赋初始值,注意如果你的父类已经调用过的话,子类不需要再重复调用。 + +@note 特别的,如果你正在为一个 UIView 子类支持 UIAppearance,不需要用到 QMUIAppearance,直接将属性、方法加上 UI_APPEARANCE_SELECTOR 标记即可,也不需要通过 -qmui_applyAppearance 的方式赋初始值(除非你希望这个赋值时机提前,系统默认时机是在 didMoveToWindow 时),系统都已经帮你处理好了,具体可查看 UIKit Documentation。 +*/ +@interface QMUIAppearance : NSObject + +/** + 获取指定 Class 的 appearance 对象,每个 Class 全局只会存在一个 appearance 对象。 + */ ++ (id)appearanceForClass:(Class)aClass; +@end + +@interface NSObject (QMUIAppearnace) + +/** + 从 appearance 里取值并赋值给当前实例,通常在对象的 init 里调用(只要在实例初始化后、使用前就可以)。适用于 QMUIAppearance 和系统的 UIAppearance。 + */ +- (void)qmui_applyAppearance; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAppearance.m b/QMUI/QMUIKit/QMUIComponents/QMUIAppearance.m new file mode 100644 index 00000000..cb712939 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIAppearance.m @@ -0,0 +1,80 @@ +/** + * 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. + */ +// +// QMUIAppearance.m +// QMUIKit +// +// Created by MoLice on 2020/3/25. +// + +#import "QMUIAppearance.h" +#import "QMUICore.h" +#import "QMUIWeakObjectContainer.h" + +@implementation QMUIAppearance + +static NSMutableDictionary *appearances; ++ (id)appearanceForClass:(Class)aClass { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (!appearances) { + appearances = NSMutableDictionary.new; + } + }); + NSString *className = NSStringFromClass(aClass); + id appearance = appearances[className]; + if (!appearance) { + BeginIgnorePerformSelectorLeaksWarning + SEL selector = NSSelectorFromString([NSString stringWithFormat:@"_%@:%@:", @"appearanceForClass", @"withContainerList"]); + appearance = [NSClassFromString(@"_UIAppearance") performSelector:selector withObject:aClass withObject:nil]; + appearances[className] = appearance; + EndIgnorePerformSelectorLeaksWarning + } + return appearance; +} + +@end + +BeginIgnoreClangWarning(-Wincomplete-implementation) +@interface NSObject (QMUIAppearance_Private) +@property(nonatomic, assign) BOOL qmui_applyingAppearance; ++ (instancetype)appearance; +@end + +@implementation NSObject (QMUIAppearnace) +QMUISynthesizeBOOLProperty(qmui_applyingAppearance, setQmui_applyingAppearance) + +/** + 关于 appearance 要考虑这几点: + 1. 是否产生内存泄漏 + 2. 父类的 appearance 能否在子类里生效 + 3. 如果某个 property 在 ClassA 里声明为 UI_APPEARANCE_SELECTOR,则在子类 Class B : Class A 里获取该 property 的值将为 nil,这是正常的,系统默认行为如此,系统是在应用 appearance 的时候发现子类的 property 值为 nil 时才会从父类里读取值,在这个阶段才完成继承效果。 + */ +- (void)qmui_applyAppearance { + Class class = self.class; + if ([class respondsToSelector:@selector(appearance)]) { + // -[_UIAppearance _applyInvocationsTo:window:] 会调用 _appearanceGuideClass,如果不是 UIView 或者 UIViewController 的子类,需要额外实现这个方法。 + SEL appearanceGuideClassSelector = NSSelectorFromString(@"_appearanceGuideClass"); + if (!class_respondsToSelector(class, appearanceGuideClassSelector)) { + const char * typeEncoding = method_getTypeEncoding(class_getInstanceMethod(UIView.class, appearanceGuideClassSelector)); + class_addMethod(class, appearanceGuideClassSelector, imp_implementationWithBlock(^Class(void) { + return nil; + }), typeEncoding); + } + + self.qmui_applyingAppearance = YES; + BeginIgnorePerformSelectorLeaksWarning + SEL selector = NSSelectorFromString([NSString stringWithFormat:@"_%@:%@:", @"applyInvocationsTo", @"window"]); + [NSClassFromString(@"_UIAppearance") performSelector:selector withObject:self withObject:nil]; + EndIgnorePerformSelectorLeaksWarning + self.qmui_applyingAppearance = NO; + } +} + +@end +EndIgnoreClangWarning diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeLabel.h b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeLabel.h new file mode 100644 index 00000000..7d589c3c --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeLabel.h @@ -0,0 +1,17 @@ +// +// QMUIBadgeLabel.h +// QMUIKit +// +// Created by molice on 2023/7/26. +// Copyright © 2023 QMUI Team. All rights reserved. +// + +#import "QMUILabel.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QMUIBadgeLabel : QMUILabel + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeLabel.m b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeLabel.m new file mode 100644 index 00000000..27e1d516 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeLabel.m @@ -0,0 +1,56 @@ +// +// QMUIBadgeLabel.m +// QMUIKit +// +// Created by molice on 2023/7/26. +// Copyright © 2023 QMUI Team. All rights reserved. +// + +#import "QMUIBadgeLabel.h" +#import "QMUICore.h" + +@implementation QMUIBadgeLabel + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.clipsToBounds = YES; + self.textAlignment = NSTextAlignmentCenter; + if (@available(iOS 13.0, *)) { + self.layer.cornerCurve = kCACornerCurveContinuous; + } + + if (QMUICMIActivated) { + self.backgroundColor = BadgeBackgroundColor; + self.textColor = BadgeTextColor; + self.font = BadgeFont; + self.contentEdgeInsets = BadgeContentEdgeInsets; + } else { + self.backgroundColor = UIColorRed; + self.textColor = UIColorWhite; + self.font = UIFontBoldMake(11); + self.contentEdgeInsets = UIEdgeInsetsMake(2, 4, 2, 4); + } + } + return self; +} + +- (CGSize)sizeThatFits:(CGSize)size { + if (self.attributedText.length == 1) { + NSMutableAttributedString *text = self.attributedText.mutableCopy; + [text replaceCharactersInRange:NSMakeRange(0, 1) withString:@"8"]; + CGSize textSize = [text boundingRectWithSize:CGSizeMax options:NSStringDrawingUsesLineFragmentOrigin context:nil].size; + CGSize result = CGSizeFlatted(CGSizeMake(textSize.width + UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets), textSize.height + UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets))); + result.width = MAX(result.width, result.height); + result.height = result.width; + return result; + } + CGSize result = [super sizeThatFits:size]; + return result; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + self.layer.cornerRadius = MIN(CGRectGetWidth(self.bounds) / 2, CGRectGetHeight(self.bounds) / 2); +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeProtocol.h b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeProtocol.h new file mode 100644 index 00000000..ee673fff --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeProtocol.h @@ -0,0 +1,83 @@ +/** + * 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. + */ + +// +// QMUIBadgeProtocol.h +// QMUIKit +// +// Created by MoLice on 2020/5/26. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol QMUIBadgeProtocol + +#pragma mark - Badge + +/// 用数字设置未读数,0表示不显示未读数。 +/// @note 仅当 qmui_badgeView 为 UILabel 及其子类时才会自动设置到 qmui_badgeView 上。 +@property(nonatomic, assign) NSUInteger qmui_badgeInteger; + +/// 用字符串设置未读数,nil 表示不显示未读数 +/// @note 仅当 qmui_badgeView 为 UILabel 及其子类时才会自动设置到 qmui_badgeView 上。 +@property(nonatomic, copy, nullable) NSString *qmui_badgeString; + +@property(nonatomic, strong, nullable) UIColor *qmui_badgeBackgroundColor; + +/// 未读数的文字颜色 +/// @note 仅当 qmui_badgeView 为 UILabel 及其子类时才会自动设置到 qmui_badgeView 上。 +@property(nonatomic, strong, nullable) UIColor *qmui_badgeTextColor; + +/// 未读数的字体 +/// @note 仅当 qmui_badgeView 为 UILabel 及其子类时才会自动设置到 qmui_badgeView 上。 +@property(nonatomic, strong, nullable) UIFont *qmui_badgeFont; + +/// 未读数字与圆圈之间的 padding,会影响最终 badge 的大小。当只有一位数字时,会取宽/高中最大的值作为最终的宽高,以保证整个 badge 是正圆。 +/// /// @note 仅当 qmui_badgeView 为 QMUILabel 及其子类时才会自动设置到 qmui_badgeView 上。 +@property(nonatomic, assign) UIEdgeInsets qmui_badgeContentEdgeInsets; + +/// 默认 badge 的布局处于 view 右上角(x = view.width, y = -badge height),通过这个属性可以调整 badge 相对于默认原点的偏移,x 正值表示向右,y 正值表示向下。 +/// 特别地,对于普通的 UITabBarItem 和 UIBarButtonItem,badge 布局相对于内部的 imageView 而不是按钮本身,如果该 item 使用了 customView 则相对于按钮本身。 +@property(nonatomic, assign) CGPoint qmui_badgeOffset; + +/// 横屏下使用,其他同 @c qmui_badgeOffset 。 +@property(nonatomic, assign) CGPoint qmui_badgeOffsetLandscape; + +/// 未读数的 view,默认是 QMUIBadgeLabel,也可设置为自定义的 view。自定义 view 如果是 UILabel 类型则内部会自动为其设置 text、textColor,但如果是其他类型的 view 则需要业务自行处理。 +@property(nonatomic, strong, nullable) __kindof UIView *qmui_badgeView; + +/// badgeView 布局完成后的回调。因为 badgeView 必定在当前 view 的 layoutSubviews 执行完之后才布局,所以业务很难在自己的 layoutSubviews 里重新调整 badgeView 的布局,所以提供一个 block。 +@property(nonatomic, copy, nullable) void (^qmui_badgeViewDidLayoutBlock)(__kindof UIView *aView, __kindof UIView *aBadgeView); + + +#pragma mark - UpdatesIndicator + +/// 控制红点的显隐 +@property(nonatomic, assign) BOOL qmui_shouldShowUpdatesIndicator; +@property(nonatomic, strong, nullable) UIColor *qmui_updatesIndicatorColor; +@property(nonatomic, assign) CGSize qmui_updatesIndicatorSize; + +/// 默认红点的布局处于 view 右上角(x = view.width, y = -badge height),通过这个属性可以调整红点相对于默认原点的偏移,x 正值表示向右,y 正值表示向下。 +/// 特别地,对于普通的 UITabBarItem 和 UIBarButtonItem,红点相对于内部的 imageView 布局而不是按钮本身,如果该 item 使用了 customView 则相对于按钮本身。 +@property(nonatomic, assign) CGPoint qmui_updatesIndicatorOffset; + +/// 横屏下使用,其他同 @c qmui_updatesIndicatorOffset 。 +@property(nonatomic, assign) CGPoint qmui_updatesIndicatorOffsetLandscape; + +/// 未读红点的 view,支持设置为自定义 view。 +@property(nonatomic, strong, nullable) __kindof UIView *qmui_updatesIndicatorView; + +/// updatesIndicatorView 布局完成后的回调。因为 updatesIndicatorView 必定在当前 view 的 layoutSubviews 执行完之后才布局,所以业务很难在自己的 layoutSubviews 里重新调整 updatesIndicatorView 的布局,所以提供一个 block。 +@property(nonatomic, copy, nullable) void (^qmui_updatesIndicatorViewDidLayoutBlock)(__kindof UIView *aView, __kindof UIView *aUpdatesIndicatorView); + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.h b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.h index 7560f16c..a5e31442 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIBarItem+QMUIBadge.h @@ -15,58 +15,14 @@ #import #import - -@class QMUILabel; +#import "QMUIBadgeProtocol.h" /** - * 用于在 UIBarButtonItem(通常用于 UINavigationBar 和 UIToolbar)和 UITabBarItem 上显示未读红点或者未读数。对设置的时机没有要求。所有属性在 QMUIConfigurationTemplate 配置表里均提供对应的默认值设置,如果你不使用配置表,则所有属性的默认值均为 0。 + * 用于在 UIBarButtonItem(通常用于 UINavigationBar 和 UIToolbar)和 UITabBarItem 上显示未读红点或者未读数,对设置的时机没有要求。 + * 提供的属性请查看 @c QMUIBadgeProtocol ,属性的默认值在 QMUIConfigurationTemplate 配置表里设置,如果不使用配置表,则所有属性的默认值均为 0 或 nil。 * * @note 系统对 UIBarButtonItem 和 UITabBarItem 在横竖屏下均会有不同的布局,当你使用本控件时建议分别检查横竖屏下的表现是否正确。 */ -@interface UIBarItem (QMUIBadge) - - -#pragma mark - Badge - -/// 用数字设置未读数,0表示不显示未读数 -@property(nonatomic, assign) NSUInteger qmui_badgeInteger; - -/// 用字符串设置未读数,nil 表示不显示未读数 -@property(nonatomic, copy, nullable) NSString *qmui_badgeString; - -@property(nonatomic, strong, nullable) UIColor *qmui_badgeBackgroundColor; -@property(nonatomic, strong, nullable) UIColor *qmui_badgeTextColor; -@property(nonatomic, strong, nullable) UIFont *qmui_badgeFont; - -/// 未读数字与圆圈之间的 padding,会影响最终 badge 的大小。当只有一位数字时,会取宽/高中最大的值作为最终的宽高,以保证整个 badge 是正圆。 -@property(nonatomic, assign) UIEdgeInsets qmui_badgeContentEdgeInsets; - -/// 默认 badge 的布局处于 item 正中心,而通过这个属性可以调整 badge 相对于默认原点的偏移,x 正值表示向右,y 正值表示向下。 -/// 特别地,对于 UITabBarItem,badge 布局默认处于内部的 imageView 的正中心(而不是 item 的正中心)。 -@property(nonatomic, assign) CGPoint qmui_badgeCenterOffset; - -/// 默认 badge 的布局处于 item 正中心,而通过这个属性可以调整横屏模式下 badge 相对于默认原点的偏移,x 正值表示向右,y 正值表示向下。 -/// 特别地,对于 UITabBarItem,badge 布局默认处于内部的 imageView 的正中心(而不是 item 的正中心)。 -@property(nonatomic, assign) CGPoint qmui_badgeCenterOffsetLandscape; - -@property(nonatomic, strong, readonly, nullable) QMUILabel *qmui_badgeLabel; - - -#pragma mark - UpdatesIndicator - -/// 控制红点的显隐 -@property(nonatomic, assign) BOOL qmui_shouldShowUpdatesIndicator; -@property(nonatomic, strong, nullable) UIColor *qmui_updatesIndicatorColor; -@property(nonatomic, assign) CGSize qmui_updatesIndicatorSize; - -/// 默认红点的布局处于 item 正中心,而通过这个属性可以调整红点相对于默认原点的偏移,x 正值表示向右,y 正值表示向下。 -/// 特别地,对于 UITabBarItem,红点布局默认处于内部的 imageView 的正中心(而不是 item 的正中心)。 -@property(nonatomic, assign) CGPoint qmui_updatesIndicatorCenterOffset; - -/// 默认红点的布局处于 item 正中心,而通过这个属性可以调整横屏模式下红点相对于默认原点的偏移,x 正值表示向右,y 正值表示向下。 -/// 特别地,对于 UITabBarItem,红点布局默认处于内部的 imageView 的正中心(而不是 item 的正中心)。 -@property(nonatomic, assign) CGPoint qmui_updatesIndicatorCenterOffsetLandscape; - -@property(nonatomic, strong, readonly, nullable) UIView *qmui_updatesIndicatorView; +@interface UIBarItem (QMUIBadge) @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m index 731f35d2..9dbb0490 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIBarItem+QMUIBadge.m @@ -15,30 +15,8 @@ #import "UIBarItem+QMUIBadge.h" #import "QMUICore.h" -#import "QMUILog.h" -#import "QMUILabel.h" -#import "UIView+QMUI.h" +#import "UIView+QMUIBadge.h" #import "UIBarItem+QMUI.h" -#import "UITabBarItem+QMUI.h" -#import "UIViewController+QMUI.h" - -@interface _QMUIBadgeLabel : QMUILabel - -@property(nonatomic, assign) CGPoint centerOffset; -@property(nonatomic, assign) CGPoint centerOffsetLandscape; -@end - -@interface _QMUIUpdatesIndicatorView : UIView - -@property(nonatomic, assign) CGPoint centerOffset; -@property(nonatomic, assign) CGPoint centerOffsetLandscape; -@end - -@interface UIBarItem () - -@property(nonatomic, strong, readwrite) _QMUIBadgeLabel *qmui_badgeLabel; -@property(nonatomic, strong, readwrite) _QMUIUpdatesIndicatorView *qmui_updatesIndicatorView; -@end @implementation UIBarItem (QMUIBadge) @@ -60,17 +38,10 @@ + (void)load { OverrideImplementation(NSClassFromString([NSString stringWithFormat:@"%@%@", @"UITab", @"BarButton"]), @selector(addSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIView *selfObject, UIView *firstArgv) { - if ([selfObject isKindOfClass:originClass] && firstArgv.superview == selfObject) { + if (firstArgv.superview == selfObject) { return; } - if (selfObject == firstArgv) { - UIViewController *visibleViewController = [QMUIHelper visibleViewController]; - NSString *log = [NSString stringWithFormat:@"UIBarItem (QMUIBadge) addSubview:, 把自己作为 subview 添加到自己身上,self = %@, visibleViewController = %@, visibleState = %@, viewControllers = %@\n%@", selfObject, visibleViewController, @(visibleViewController.qmui_visibleState), visibleViewController.navigationController.viewControllers, [NSThread callStackSymbols]]; - NSAssert(NO, log); - QMUILogWarn(@"UIBarItem (QMUIBadge)", @"%@", log); - } - // call super IMP originalIMP = originalIMPProvider(); void (*originSelectorIMP)(id, SEL, UIView *); @@ -82,17 +53,19 @@ + (void)load { } - (void)qmuibaritem_didInitialize { - self.qmui_badgeBackgroundColor = BadgeBackgroundColor; - self.qmui_badgeTextColor = BadgeTextColor; - self.qmui_badgeFont = BadgeFont; - self.qmui_badgeContentEdgeInsets = BadgeContentEdgeInsets; - self.qmui_badgeCenterOffset = BadgeCenterOffset; - self.qmui_badgeCenterOffsetLandscape = BadgeCenterOffsetLandscape; - - self.qmui_updatesIndicatorColor = UpdatesIndicatorColor; - self.qmui_updatesIndicatorSize = UpdatesIndicatorSize; - self.qmui_updatesIndicatorCenterOffset = UpdatesIndicatorCenterOffset; - self.qmui_updatesIndicatorCenterOffsetLandscape = UpdatesIndicatorCenterOffsetLandscape; + if (QMUICMIActivated) { + self.qmui_badgeBackgroundColor = BadgeBackgroundColor; + self.qmui_badgeTextColor = BadgeTextColor; + self.qmui_badgeFont = BadgeFont; + self.qmui_badgeContentEdgeInsets = BadgeContentEdgeInsets; + self.qmui_badgeOffset = BadgeOffset; + self.qmui_badgeOffsetLandscape = BadgeOffsetLandscape; + + self.qmui_updatesIndicatorColor = UpdatesIndicatorColor; + self.qmui_updatesIndicatorSize = UpdatesIndicatorSize; + self.qmui_updatesIndicatorOffset = UpdatesIndicatorOffset; + self.qmui_updatesIndicatorOffsetLandscape = UpdatesIndicatorOffsetLandscape; + } } #pragma mark - Badge @@ -111,40 +84,9 @@ - (NSUInteger)qmui_badgeInteger { - (void)setQmui_badgeString:(NSString *)qmui_badgeString { objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeString, qmui_badgeString, OBJC_ASSOCIATION_COPY_NONATOMIC); if (qmui_badgeString.length) { - if (!self.qmui_badgeLabel) { - self.qmui_badgeLabel = [[_QMUIBadgeLabel alloc] init]; - self.qmui_badgeLabel.clipsToBounds = YES; - self.qmui_badgeLabel.textAlignment = NSTextAlignmentCenter; - self.qmui_badgeLabel.backgroundColor = self.qmui_badgeBackgroundColor; - self.qmui_badgeLabel.textColor = self.qmui_badgeTextColor; - self.qmui_badgeLabel.font = self.qmui_badgeFont; - self.qmui_badgeLabel.contentEdgeInsets = self.qmui_badgeContentEdgeInsets; - self.qmui_badgeLabel.centerOffset = self.qmui_badgeCenterOffset; - self.qmui_badgeLabel.centerOffsetLandscape = self.qmui_badgeCenterOffsetLandscape; - if (!self.qmui_viewDidSetBlock) { - self.qmui_viewDidSetBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) { - [view addSubview:item.qmui_updatesIndicatorView]; - [view addSubview:item.qmui_badgeLabel]; - [view setNeedsLayout]; - [view layoutIfNeeded]; - }; - } - // 之前 item 已经 set 完 view,则手动触发一次 - if (self.qmui_view) { - self.qmui_viewDidSetBlock(self, self.qmui_view); - } - if (!self.qmui_viewDidLayoutSubviewsBlock) { - self.qmui_viewDidLayoutSubviewsBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) { - [item layoutSubviews]; - }; - } - } - self.qmui_badgeLabel.text = qmui_badgeString; - self.qmui_badgeLabel.hidden = NO; - [self setNeedsUpdateBadgeLabelLayout]; - } else { - self.qmui_badgeLabel.hidden = YES; + [self updateViewDidSetBlockIfNeeded]; } + self.qmui_view.qmui_badgeString = qmui_badgeString; } - (NSString *)qmui_badgeString { @@ -154,9 +96,7 @@ - (NSString *)qmui_badgeString { static char kAssociatedObjectKey_badgeBackgroundColor; - (void)setQmui_badgeBackgroundColor:(UIColor *)qmui_badgeBackgroundColor { objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeBackgroundColor, qmui_badgeBackgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (self.qmui_badgeLabel) { - self.qmui_badgeLabel.backgroundColor = qmui_badgeBackgroundColor; - } + self.qmui_view.qmui_badgeBackgroundColor = qmui_badgeBackgroundColor; } - (UIColor *)qmui_badgeBackgroundColor { @@ -166,9 +106,7 @@ - (UIColor *)qmui_badgeBackgroundColor { static char kAssociatedObjectKey_badgeTextColor; - (void)setQmui_badgeTextColor:(UIColor *)qmui_badgeTextColor { objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeTextColor, qmui_badgeTextColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (self.qmui_badgeLabel) { - self.qmui_badgeLabel.textColor = qmui_badgeTextColor; - } + self.qmui_view.qmui_badgeTextColor = qmui_badgeTextColor; } - (UIColor *)qmui_badgeTextColor { @@ -178,10 +116,7 @@ - (UIColor *)qmui_badgeTextColor { static char kAssociatedObjectKey_badgeFont; - (void)setQmui_badgeFont:(UIFont *)qmui_badgeFont { objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeFont, qmui_badgeFont, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (self.qmui_badgeLabel) { - self.qmui_badgeLabel.font = qmui_badgeFont; - [self setNeedsUpdateBadgeLabelLayout]; - } + self.qmui_view.qmui_badgeFont = qmui_badgeFont; } - (UIFont *)qmui_badgeFont { @@ -191,53 +126,47 @@ - (UIFont *)qmui_badgeFont { static char kAssociatedObjectKey_badgeContentEdgeInsets; - (void)setQmui_badgeContentEdgeInsets:(UIEdgeInsets)qmui_badgeContentEdgeInsets { objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeContentEdgeInsets, [NSValue valueWithUIEdgeInsets:qmui_badgeContentEdgeInsets], OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (self.qmui_badgeLabel) { - self.qmui_badgeLabel.contentEdgeInsets = qmui_badgeContentEdgeInsets; - [self setNeedsUpdateBadgeLabelLayout]; - } + self.qmui_view.qmui_badgeContentEdgeInsets = qmui_badgeContentEdgeInsets; } - (UIEdgeInsets)qmui_badgeContentEdgeInsets { return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeContentEdgeInsets)) UIEdgeInsetsValue]; } -static char kAssociatedObjectKey_badgeCenterOffset; -- (void)setQmui_badgeCenterOffset:(CGPoint)qmui_badgeCenterOffset { - objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffset, [NSValue valueWithCGPoint:qmui_badgeCenterOffset], OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (self.qmui_badgeLabel) { - self.qmui_badgeLabel.centerOffset = qmui_badgeCenterOffset; - } +static char kAssociatedObjectKey_badgeOffset; +- (void)setQmui_badgeOffset:(CGPoint)qmui_badgeOffset { + objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeOffset, @(qmui_badgeOffset), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + self.qmui_view.qmui_badgeOffset = qmui_badgeOffset; } -- (CGPoint)qmui_badgeCenterOffset { - return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffset)) CGPointValue]; +- (CGPoint)qmui_badgeOffset { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeOffset)) CGPointValue]; } -static char kAssociatedObjectKey_badgeCenterOffsetLandscape; -- (void)setQmui_badgeCenterOffsetLandscape:(CGPoint)qmui_badgeCenterOffsetLandscape { - objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffsetLandscape, [NSValue valueWithCGPoint:qmui_badgeCenterOffsetLandscape], OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (self.qmui_badgeLabel) { - self.qmui_badgeLabel.centerOffsetLandscape = qmui_badgeCenterOffsetLandscape; - } +static char kAssociatedObjectKey_badgeOffsetLandscape; +- (void)setQmui_badgeOffsetLandscape:(CGPoint)qmui_badgeOffsetLandscape { + objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeOffsetLandscape, @(qmui_badgeOffsetLandscape), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + self.qmui_view.qmui_badgeOffsetLandscape = qmui_badgeOffsetLandscape; } -- (CGPoint)qmui_badgeCenterOffsetLandscape { - return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffsetLandscape)) CGPointValue]; +- (CGPoint)qmui_badgeOffsetLandscape { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeOffsetLandscape)) CGPointValue]; } -static char kAssociatedObjectKey_badgeLabel; -- (void)setQmui_badgeLabel:(UILabel *)qmui_badgeLabel { - objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeLabel, qmui_badgeLabel, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +- (void)setQmui_badgeView:(__kindof UIView *)qmui_badgeView { + self.qmui_view.qmui_badgeView = qmui_badgeView; } -- (_QMUIBadgeLabel *)qmui_badgeLabel { - return (_QMUIBadgeLabel *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeLabel); +- (__kindof UIView *)qmui_badgeView { + return self.qmui_view.qmui_badgeView; } -- (void)setNeedsUpdateBadgeLabelLayout { - if (self.qmui_badgeString.length) { - [self.qmui_view setNeedsLayout]; - } +- (void)setQmui_badgeViewDidLayoutBlock:(void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_badgeViewDidLayoutBlock { + self.qmui_view.qmui_badgeViewDidLayoutBlock = qmui_badgeViewDidLayoutBlock; +} + +- (void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_badgeViewDidLayoutBlock { + return self.qmui_view.qmui_badgeViewDidLayoutBlock; } #pragma mark - UpdatesIndicator @@ -246,35 +175,9 @@ - (void)setNeedsUpdateBadgeLabelLayout { - (void)setQmui_shouldShowUpdatesIndicator:(BOOL)qmui_shouldShowUpdatesIndicator { objc_setAssociatedObject(self, &kAssociatedObjectKey_shouldShowUpdatesIndicator, @(qmui_shouldShowUpdatesIndicator), OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (qmui_shouldShowUpdatesIndicator) { - if (!self.qmui_updatesIndicatorView) { - self.qmui_updatesIndicatorView = [[_QMUIUpdatesIndicatorView alloc] qmui_initWithSize:self.qmui_updatesIndicatorSize]; - self.qmui_updatesIndicatorView.layer.cornerRadius = CGRectGetHeight(self.qmui_updatesIndicatorView.bounds) / 2; - self.qmui_updatesIndicatorView.backgroundColor = self.qmui_updatesIndicatorColor; - self.qmui_updatesIndicatorView.centerOffset = self.qmui_updatesIndicatorCenterOffset; - self.qmui_updatesIndicatorView.centerOffsetLandscape = self.qmui_updatesIndicatorCenterOffsetLandscape; - if (!self.qmui_viewDidLayoutSubviewsBlock) { - self.qmui_viewDidLayoutSubviewsBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) { - [item layoutSubviews]; - }; - } - if (!self.qmui_viewDidSetBlock) { - self.qmui_viewDidSetBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) { - [view addSubview:item.qmui_updatesIndicatorView]; - [view addSubview:item.qmui_badgeLabel]; - [view setNeedsLayout]; - [view layoutIfNeeded]; - }; - } - // 之前 item 已经 set 完 view,则手动触发一次 - if (self.qmui_view) { - self.qmui_viewDidSetBlock(self, self.qmui_view); - } - } - [self setNeedsUpdateIndicatorLayout]; - self.qmui_updatesIndicatorView.hidden = NO; - } else { - self.qmui_updatesIndicatorView.hidden = YES; + [self updateViewDidSetBlockIfNeeded]; } + self.qmui_view.qmui_shouldShowUpdatesIndicator = qmui_shouldShowUpdatesIndicator; } - (BOOL)qmui_shouldShowUpdatesIndicator { @@ -284,9 +187,7 @@ - (BOOL)qmui_shouldShowUpdatesIndicator { static char kAssociatedObjectKey_updatesIndicatorColor; - (void)setQmui_updatesIndicatorColor:(UIColor *)qmui_updatesIndicatorColor { objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorColor, qmui_updatesIndicatorColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (self.qmui_updatesIndicatorView) { - self.qmui_updatesIndicatorView.backgroundColor = qmui_updatesIndicatorColor; - } + self.qmui_view.qmui_updatesIndicatorColor = qmui_updatesIndicatorColor; } - (UIColor *)qmui_updatesIndicatorColor { @@ -296,146 +197,75 @@ - (UIColor *)qmui_updatesIndicatorColor { static char kAssociatedObjectKey_updatesIndicatorSize; - (void)setQmui_updatesIndicatorSize:(CGSize)qmui_updatesIndicatorSize { objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorSize, [NSValue valueWithCGSize:qmui_updatesIndicatorSize], OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (self.qmui_updatesIndicatorView) { - self.qmui_updatesIndicatorView.frame = CGRectSetSize(self.qmui_updatesIndicatorView.frame, qmui_updatesIndicatorSize); - self.qmui_updatesIndicatorView.layer.cornerRadius = qmui_updatesIndicatorSize.height / 2; - [self setNeedsUpdateIndicatorLayout]; - } + self.qmui_view.qmui_updatesIndicatorSize = qmui_updatesIndicatorSize; } - (CGSize)qmui_updatesIndicatorSize { return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorSize)) CGSizeValue]; } -static char kAssociatedObjectKey_updatesIndicatorCenterOffset; -- (void)setQmui_updatesIndicatorCenterOffset:(CGPoint)qmui_updatesIndicatorCenterOffset { - objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffset, [NSValue valueWithCGPoint:qmui_updatesIndicatorCenterOffset], OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (self.qmui_updatesIndicatorView) { - self.qmui_updatesIndicatorView.centerOffset = qmui_updatesIndicatorCenterOffset; - [self setNeedsUpdateIndicatorLayout]; - } +static char kAssociatedObjectKey_updatesIndicatorOffset; +- (void)setQmui_updatesIndicatorOffset:(CGPoint)qmui_updatesIndicatorOffset { + objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffset, @(qmui_updatesIndicatorOffset), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + self.qmui_view.qmui_updatesIndicatorOffset = qmui_updatesIndicatorOffset; } -- (CGPoint)qmui_updatesIndicatorCenterOffset { - return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffset)) CGPointValue]; +- (CGPoint)qmui_updatesIndicatorOffset { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffset)) CGPointValue]; } -static char kAssociatedObjectKey_updatesIndicatorCenterOffsetLandscape; -- (void)setQmui_updatesIndicatorCenterOffsetLandscape:(CGPoint)qmui_updatesIndicatorCenterOffsetLandscape { - objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffsetLandscape, [NSValue valueWithCGPoint:qmui_updatesIndicatorCenterOffsetLandscape], OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (self.qmui_updatesIndicatorView) { - self.qmui_updatesIndicatorView.centerOffsetLandscape = qmui_updatesIndicatorCenterOffsetLandscape; - [self setNeedsUpdateIndicatorLayout]; - } +static char kAssociatedObjectKey_updatesIndicatorOffsetLandscape; +- (void)setQmui_updatesIndicatorOffsetLandscape:(CGPoint)qmui_updatesIndicatorOffsetLandscape { + objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffsetLandscape, @(qmui_updatesIndicatorOffsetLandscape), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + self.qmui_view.qmui_updatesIndicatorOffsetLandscape = qmui_updatesIndicatorOffsetLandscape; } -- (CGPoint)qmui_updatesIndicatorCenterOffsetLandscape { - return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffsetLandscape)) CGPointValue]; +- (CGPoint)qmui_updatesIndicatorOffsetLandscape { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffsetLandscape)) CGPointValue]; } -static char kAssociatedObjectKey_updatesIndicatorView; -- (void)setQmui_updatesIndicatorView:(UIView *)qmui_updatesIndicatorView { - objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorView, qmui_updatesIndicatorView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +- (void)setQmui_updatesIndicatorView:(__kindof UIView *)qmui_updatesIndicatorView { + self.qmui_view.qmui_updatesIndicatorView = qmui_updatesIndicatorView; } -- (_QMUIUpdatesIndicatorView *)qmui_updatesIndicatorView { - return (_QMUIUpdatesIndicatorView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorView); +- (UIView *)qmui_updatesIndicatorView { + return self.qmui_view.qmui_updatesIndicatorView; } -- (void)setNeedsUpdateIndicatorLayout { - if (self.qmui_shouldShowUpdatesIndicator) { - [self.qmui_view setNeedsLayout]; - } +- (void)setQmui_updatesIndicatorViewDidLayoutBlock:(void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_updatesIndicatorViewDidLayoutBlock { + self.qmui_view.qmui_updatesIndicatorViewDidLayoutBlock = qmui_updatesIndicatorViewDidLayoutBlock; } -#pragma mark - Common - -- (void)layoutSubviews { - - if (self.qmui_updatesIndicatorView && !self.qmui_updatesIndicatorView.hidden) { - CGPoint centerOffset = IS_LANDSCAPE ? self.qmui_updatesIndicatorView.centerOffsetLandscape : self.qmui_updatesIndicatorView.centerOffset; - - UIView *superview = self.qmui_updatesIndicatorView.superview; - if ([NSStringFromClass(superview.class) hasPrefix:@"UITabBar"]) { - // 特别的,对于 UITabBarItem,将 imageView 的 center 作为参考点 - UIView *imageView = [UITabBarItem qmui_imageViewInTabBarButton:superview]; - if (!imageView) return; +- (void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_updatesIndicatorViewDidLayoutBlock { + return self.qmui_view.qmui_updatesIndicatorViewDidLayoutBlock; +} - self.qmui_updatesIndicatorView.frame = CGRectSetXY(self.qmui_updatesIndicatorView.frame, CGRectGetMinXHorizontallyCenter(imageView.frame, self.qmui_updatesIndicatorView.frame) + centerOffset.x, CGRectGetMinYVerticallyCenter(imageView.frame, self.qmui_updatesIndicatorView.frame) + centerOffset.y); - } else { - self.qmui_updatesIndicatorView.frame = CGRectSetXY(self.qmui_updatesIndicatorView.frame, CGFloatGetCenter(superview.qmui_width, self.qmui_updatesIndicatorView.qmui_width) + centerOffset.x, CGFloatGetCenter(superview.qmui_height, self.qmui_updatesIndicatorView.qmui_height) + centerOffset.y); - } +#pragma mark - Common - [superview bringSubviewToFront:self.qmui_updatesIndicatorView]; - } - - if (self.qmui_badgeLabel && !self.qmui_badgeLabel.hidden) { - [self.qmui_badgeLabel sizeToFit]; - self.qmui_badgeLabel.layer.cornerRadius = MIN(self.qmui_badgeLabel.qmui_height / 2, self.qmui_badgeLabel.qmui_width / 2); - - CGPoint centerOffset = IS_LANDSCAPE ? self.qmui_badgeLabel.centerOffsetLandscape : self.qmui_badgeLabel.centerOffset; - - UIView *superview = self.qmui_badgeLabel.superview; - if ([NSStringFromClass(superview.class) hasPrefix:@"UITabBar"]) { - // 特别的,对于 UITabBarItem,将 imageView 的 center 作为参考点 - UIView *imageView = [UITabBarItem qmui_imageViewInTabBarButton:superview]; - if (!imageView) return; +- (void)updateViewDidSetBlockIfNeeded { + if (!self.qmui_viewDidSetBlock) { + self.qmui_viewDidSetBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) { + view.qmui_badgeBackgroundColor = item.qmui_badgeBackgroundColor; + view.qmui_badgeTextColor = item.qmui_badgeTextColor; + view.qmui_badgeFont = item.qmui_badgeFont; + view.qmui_badgeContentEdgeInsets = item.qmui_badgeContentEdgeInsets; + view.qmui_badgeOffset = item.qmui_badgeOffset; + view.qmui_badgeOffsetLandscape = item.qmui_badgeOffsetLandscape; - self.qmui_badgeLabel.frame = CGRectSetXY(self.qmui_badgeLabel.frame, CGRectGetMinXHorizontallyCenter(imageView.frame, self.qmui_badgeLabel.frame) + centerOffset.x, CGRectGetMinYVerticallyCenter(imageView.frame, self.qmui_badgeLabel.frame) + centerOffset.y); - } else { - self.qmui_badgeLabel.frame = CGRectSetXY(self.qmui_badgeLabel.frame, CGFloatGetCenter(superview.qmui_width, self.qmui_badgeLabel.qmui_width) + centerOffset.x, CGFloatGetCenter(superview.qmui_height, self.qmui_badgeLabel.qmui_height) + centerOffset.y); - } + view.qmui_updatesIndicatorColor = item.qmui_updatesIndicatorColor; + view.qmui_updatesIndicatorSize = item.qmui_updatesIndicatorSize; + view.qmui_updatesIndicatorOffset = item.qmui_updatesIndicatorOffset; + view.qmui_updatesIndicatorOffsetLandscape = item.qmui_updatesIndicatorOffsetLandscape; + + view.qmui_badgeString = item.qmui_badgeString; + view.qmui_shouldShowUpdatesIndicator = item.qmui_shouldShowUpdatesIndicator; + }; - [superview bringSubviewToFront:self.qmui_badgeLabel]; - } -} - -@end - -@implementation _QMUIUpdatesIndicatorView - -- (void)setCenterOffset:(CGPoint)centerOffset { - _centerOffset = centerOffset; - if (!IS_LANDSCAPE) { - [self.superview setNeedsLayout]; - } -} - -- (void)setCenterOffsetLandscape:(CGPoint)centerOffsetLandscape { - _centerOffsetLandscape = centerOffsetLandscape; - if (IS_LANDSCAPE) { - [self.superview setNeedsLayout]; - } -} - -@end - -@implementation _QMUIBadgeLabel - -- (void)setCenterOffset:(CGPoint)centerOffset { - _centerOffset = centerOffset; - if (!IS_LANDSCAPE) { - [self.superview setNeedsLayout]; - } -} - -- (void)setCenterOffsetLandscape:(CGPoint)centerOffsetLandscape { - _centerOffsetLandscape = centerOffsetLandscape; - if (IS_LANDSCAPE) { - [self.superview setNeedsLayout]; - } -} - -- (CGSize)sizeThatFits:(CGSize)size { - CGSize result = [super sizeThatFits:size]; - - // 只有一个字的时候保证它是一个正方形 - if (self.text.length <= 1) { - CGFloat finalSize = MAX(result.width, result.height); - result = CGSizeMake(finalSize, finalSize); + // 为 qmui_viewDidSetBlock 赋值前 item 已经 set 完 view,则手动触发一次 + if (self.qmui_view) { + self.qmui_viewDidSetBlock(self, self.qmui_view); + } } - - return result; } @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.h b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.h new file mode 100644 index 00000000..b56b7bc4 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.h @@ -0,0 +1,30 @@ +/** + * 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. + */ + +// +// UIView+QMUIBadge.h +// QMUIKit +// +// Created by MoLice on 2020/5/26. +// + +#import +#import "QMUIBadgeProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + 用于在任意 UIView 上显示未读红点或者未读数,提供的属性请查看 @c QMUIBadgeProtocol ,属性的默认值在 QMUIConfigurationTemplate 配置表里设置,如果不使用配置表,则所有属性的默认值均为 0 或 nil。 + + @note 使用该组件会强制设置 view.clipsToBounds = NO 以避免布局到 view 外部的红点/未读数看不到。 + */ +@interface UIView (QMUIBadge) + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m new file mode 100644 index 00000000..1bb929a5 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m @@ -0,0 +1,376 @@ +/** + * 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. + */ + +// +// UIView+QMUIBadge.m +// QMUIKit +// +// Created by MoLice on 2020/5/26. +// + +#import "UIView+QMUIBadge.h" +#import "QMUICore.h" +#import "QMUILabel.h" +#import "UIView+QMUI.h" +#import "UITabBarItem+QMUI.h" +#import "QMUIBadgeLabel.h" + +@interface UIView () +@property(nullable, nonatomic, strong) void (^qmuibdg_layoutSubviewsBlock)(__kindof UIView *view); +@end + +@implementation UIView (QMUIBadge) + +QMUISynthesizeIdStrongProperty(qmuibdg_layoutSubviewsBlock, setQmuibdg_layoutSubviewsBlock) +QMUISynthesizeIdCopyProperty(qmui_badgeViewDidLayoutBlock, setQmui_badgeViewDidLayoutBlock) +QMUISynthesizeIdCopyProperty(qmui_updatesIndicatorViewDidLayoutBlock, setQmui_updatesIndicatorViewDidLayoutBlock) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // 保证配置表里的默认值正确被设置 + ExtendImplementationOfNonVoidMethodWithSingleArgument([UIView class], @selector(initWithFrame:), CGRect, UIView *, ^UIView *(UIView *selfObject, CGRect firstArgv, UIView *originReturnValue) { + [selfObject qmuibdg_didInitialize]; + return originReturnValue; + }); + + ExtendImplementationOfNonVoidMethodWithSingleArgument([UIView class], @selector(initWithCoder:), NSCoder *, UIView *, ^UIView *(UIView *selfObject, NSCoder *firstArgv, UIView *originReturnValue) { + [selfObject qmuibdg_didInitialize]; + return originReturnValue; + }); + + OverrideImplementation([UIView class], @selector(setQmui_layoutSubviewsBlock:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, void (^firstArgv)(__kindof UIView *aView)) { + + if (firstArgv && selfObject.qmuibdg_layoutSubviewsBlock && firstArgv != selfObject.qmuibdg_layoutSubviewsBlock) { + firstArgv = ^void(__kindof UIView *aaView) { + firstArgv(aaView); + aaView.qmuibdg_layoutSubviewsBlock(aaView); + }; + } + + // call super + void (*originSelectorIMP)(id, SEL, void (^firstArgv)(__kindof UIView *aView)); + originSelectorIMP = (void (*)(id, SEL, void (^firstArgv)(__kindof UIView *aView)))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + }; + }); + }); +} + +- (void)qmuibdg_didInitialize { + if (QMUICMIActivated) { + self.qmui_badgeBackgroundColor = BadgeBackgroundColor; + self.qmui_badgeTextColor = BadgeTextColor; + self.qmui_badgeFont = BadgeFont; + self.qmui_badgeContentEdgeInsets = BadgeContentEdgeInsets; + self.qmui_badgeOffset = BadgeOffset; + self.qmui_badgeOffsetLandscape = BadgeOffsetLandscape; + + self.qmui_updatesIndicatorColor = UpdatesIndicatorColor; + self.qmui_updatesIndicatorSize = UpdatesIndicatorSize; + self.qmui_updatesIndicatorOffset = UpdatesIndicatorOffset; + self.qmui_updatesIndicatorOffsetLandscape = UpdatesIndicatorOffsetLandscape; + } +} + +#pragma mark - Badge + +static char kAssociatedObjectKey_badgeInteger; +- (void)setQmui_badgeInteger:(NSUInteger)qmui_badgeInteger { + objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeInteger, @(qmui_badgeInteger), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + self.qmui_badgeString = qmui_badgeInteger > 0 ? [NSString stringWithFormat:@"%@", @(qmui_badgeInteger)] : nil; +} + +- (NSUInteger)qmui_badgeInteger { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeInteger)) unsignedIntegerValue]; +} + +static char kAssociatedObjectKey_badgeString; +- (void)setQmui_badgeString:(NSString *)qmui_badgeString { + objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeString, qmui_badgeString, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_badgeString.length) { + if (!self.qmui_badgeView) { + QMUIBadgeLabel *badgeLabel = [[QMUIBadgeLabel alloc] init]; + badgeLabel.backgroundColor = self.qmui_badgeBackgroundColor; + badgeLabel.textColor = self.qmui_badgeTextColor; + badgeLabel.font = self.qmui_badgeFont; + badgeLabel.contentEdgeInsets = self.qmui_badgeContentEdgeInsets; + self.qmui_badgeView = badgeLabel; + } + if ([self.qmui_badgeView respondsToSelector:@selector(setText:)]) { + ((UILabel *)self.qmui_badgeView).text = qmui_badgeString; + } + self.qmui_badgeView.hidden = NO; + [self setNeedsUpdateBadgeLabelLayout]; + QMUIAssert(!self.clipsToBounds, @"QMUIBadge", @"clipsToBounds should be NO when showing badgeString"); + self.clipsToBounds = NO; + } else { + self.qmui_badgeView.hidden = YES; + } +} + +- (NSString *)qmui_badgeString { + return (NSString *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeString); +} + +static char kAssociatedObjectKey_badgeBackgroundColor; +- (void)setQmui_badgeBackgroundColor:(UIColor *)qmui_badgeBackgroundColor { + objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeBackgroundColor, qmui_badgeBackgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + self.qmui_badgeView.backgroundColor = qmui_badgeBackgroundColor; +} + +- (UIColor *)qmui_badgeBackgroundColor { + return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeBackgroundColor); +} + +static char kAssociatedObjectKey_badgeTextColor; +- (void)setQmui_badgeTextColor:(UIColor *)qmui_badgeTextColor { + objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeTextColor, qmui_badgeTextColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if ([self.qmui_badgeView isKindOfClass:UILabel.class]) { + ((UILabel *)self.qmui_badgeView).textColor = qmui_badgeTextColor; + } +} + +- (UIColor *)qmui_badgeTextColor { + return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeTextColor); +} + +static char kAssociatedObjectKey_badgeFont; +- (void)setQmui_badgeFont:(UIFont *)qmui_badgeFont { + objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeFont, qmui_badgeFont, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if ([self.qmui_badgeView isKindOfClass:UILabel.class]) { + ((UILabel *)self.qmui_badgeView).font = qmui_badgeFont; + [self setNeedsUpdateBadgeLabelLayout]; + } +} + +- (UIFont *)qmui_badgeFont { + return (UIFont *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeFont); +} + +static char kAssociatedObjectKey_badgeContentEdgeInsets; +- (void)setQmui_badgeContentEdgeInsets:(UIEdgeInsets)qmui_badgeContentEdgeInsets { + objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeContentEdgeInsets, [NSValue valueWithUIEdgeInsets:qmui_badgeContentEdgeInsets], OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if ([self.qmui_badgeView isKindOfClass:QMUILabel.class]) { + ((QMUILabel *)self.qmui_badgeView).contentEdgeInsets = qmui_badgeContentEdgeInsets; + [self setNeedsUpdateBadgeLabelLayout]; + } +} + +- (UIEdgeInsets)qmui_badgeContentEdgeInsets { + return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeContentEdgeInsets)) UIEdgeInsetsValue]; +} + +static char kAssociatedObjectKey_badgeOffset; +- (void)setQmui_badgeOffset:(CGPoint)qmui_badgeOffset { + objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeOffset, @(qmui_badgeOffset), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self setNeedsUpdateBadgeLabelLayout]; +} + +- (CGPoint)qmui_badgeOffset { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeOffset)) CGPointValue]; +} + +static char kAssociatedObjectKey_badgeOffsetLandscape; +- (void)setQmui_badgeOffsetLandscape:(CGPoint)qmui_badgeOffsetLandscape { + objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeOffsetLandscape, @(qmui_badgeOffsetLandscape), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self setNeedsUpdateBadgeLabelLayout]; +} + +- (CGPoint)qmui_badgeOffsetLandscape { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeOffsetLandscape)) CGPointValue]; +} + +static char kAssociatedObjectKey_badgeView; +- (void)setQmui_badgeView:(UIView *)qmui_badgeView { + objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeView, qmui_badgeView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_badgeView) { + [self updateLayoutSubviewsBlockIfNeeded]; + [self addSubview:qmui_badgeView]; + [self setNeedsUpdateBadgeLabelLayout]; + } +} + +- (__kindof UIView *)qmui_badgeView { + return (UIView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeView); +} + +- (void)setNeedsUpdateBadgeLabelLayout { + if (self.qmui_badgeView && !self.qmui_badgeView.hidden) { + [self qmuibdg_layoutSubviews]; + } +} + +#pragma mark - UpdatesIndicator + +static char kAssociatedObjectKey_shouldShowUpdatesIndicator; +- (void)setQmui_shouldShowUpdatesIndicator:(BOOL)qmui_shouldShowUpdatesIndicator { + objc_setAssociatedObject(self, &kAssociatedObjectKey_shouldShowUpdatesIndicator, @(qmui_shouldShowUpdatesIndicator), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_shouldShowUpdatesIndicator) { + if (!self.qmui_updatesIndicatorView) { + self.qmui_updatesIndicatorView = [[UIView alloc] qmui_initWithSize:self.qmui_updatesIndicatorSize]; + self.qmui_updatesIndicatorView.layer.cornerRadius = CGRectGetHeight(self.qmui_updatesIndicatorView.bounds) / 2; + self.qmui_updatesIndicatorView.backgroundColor = self.qmui_updatesIndicatorColor; + } + [self setNeedsUpdateIndicatorLayout]; + QMUIAssert(!self.clipsToBounds, @"QMUIBadge", @"clipsToBounds should be NO when showing updatesIndicator"); + self.clipsToBounds = NO; + self.qmui_updatesIndicatorView.hidden = NO; + } else { + self.qmui_updatesIndicatorView.hidden = YES; + } +} + +- (BOOL)qmui_shouldShowUpdatesIndicator { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_shouldShowUpdatesIndicator)) boolValue]; +} + +static char kAssociatedObjectKey_updatesIndicatorColor; +- (void)setQmui_updatesIndicatorColor:(UIColor *)qmui_updatesIndicatorColor { + objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorColor, qmui_updatesIndicatorColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + self.qmui_updatesIndicatorView.backgroundColor = qmui_updatesIndicatorColor; +} + +- (UIColor *)qmui_updatesIndicatorColor { + return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorColor); +} + +static char kAssociatedObjectKey_updatesIndicatorSize; +- (void)setQmui_updatesIndicatorSize:(CGSize)qmui_updatesIndicatorSize { + objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorSize, [NSValue valueWithCGSize:qmui_updatesIndicatorSize], OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (self.qmui_updatesIndicatorView) { + self.qmui_updatesIndicatorView.frame = CGRectSetSize(self.qmui_updatesIndicatorView.frame, qmui_updatesIndicatorSize); + self.qmui_updatesIndicatorView.layer.cornerRadius = qmui_updatesIndicatorSize.height / 2; + [self setNeedsUpdateIndicatorLayout]; + } +} + +- (CGSize)qmui_updatesIndicatorSize { + return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorSize)) CGSizeValue]; +} + +static char kAssociatedObjectKey_updatesIndicatorOffset; +- (void)setQmui_updatesIndicatorOffset:(CGPoint)qmui_updatesIndicatorOffset { + objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffset, @(qmui_updatesIndicatorOffset), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (self.qmui_updatesIndicatorView) { + [self setNeedsUpdateIndicatorLayout]; + } +} + +- (CGPoint)qmui_updatesIndicatorOffset { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffset)) CGPointValue]; +} + +static char kAssociatedObjectKey_updatesIndicatorOffsetLandscape; +- (void)setQmui_updatesIndicatorOffsetLandscape:(CGPoint)qmui_updatesIndicatorOffsetLandscape { + objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffsetLandscape, @(qmui_updatesIndicatorOffsetLandscape), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (self.qmui_updatesIndicatorView) { + [self setNeedsUpdateIndicatorLayout]; + } +} + +- (CGPoint)qmui_updatesIndicatorOffsetLandscape { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffsetLandscape)) CGPointValue]; +} + +static char kAssociatedObjectKey_updatesIndicatorView; +- (void)setQmui_updatesIndicatorView:(__kindof UIView *)qmui_updatesIndicatorView { + objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorView, qmui_updatesIndicatorView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_updatesIndicatorView) { + [self updateLayoutSubviewsBlockIfNeeded]; + [self addSubview:qmui_updatesIndicatorView]; + [self setNeedsUpdateIndicatorLayout]; + } +} + +- (__kindof UIView *)qmui_updatesIndicatorView { + return (UIView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorView); +} + +- (void)setNeedsUpdateIndicatorLayout { + if (self.qmui_shouldShowUpdatesIndicator) { + [self qmuibdg_layoutSubviews]; + } +} + +#pragma mark - Common + +- (void)updateLayoutSubviewsBlockIfNeeded { + if (!self.qmuibdg_layoutSubviewsBlock) { + self.qmuibdg_layoutSubviewsBlock = ^(UIView *view) { + [view qmuibdg_layoutSubviews]; + }; + } + if (!self.qmui_layoutSubviewsBlock) { + self.qmui_layoutSubviewsBlock = self.qmuibdg_layoutSubviewsBlock; + } else if (self.qmui_layoutSubviewsBlock != self.qmuibdg_layoutSubviewsBlock) { + void (^originalLayoutSubviewsBlock)(__kindof UIView *) = self.qmui_layoutSubviewsBlock; + self.qmuibdg_layoutSubviewsBlock = ^(__kindof UIView *view) { + originalLayoutSubviewsBlock(view); + [view qmuibdg_layoutSubviews]; + }; + self.qmui_layoutSubviewsBlock = self.qmuibdg_layoutSubviewsBlock; + } +} + +// 不管 image 还是 text 的 UIBarButtonItem 都获取内部的 _UIModernBarButton 即可 +- (UIView *)findBarButtonContentView { + NSString *classString = NSStringFromClass(self.class); + if ([classString isEqualToString:@"UITabBarButton"]) { + // 特别的,对于 UITabBarItem,将 imageView 作为参考 view + UIView *imageView = [UITabBarItem qmui_imageViewInTabBarButton:self]; + return imageView; + } + + if ([classString isEqualToString:@"_UIButtonBarButton"]) { + for (UIView *subview in self.subviews) { + if ([subview isKindOfClass:UIButton.class]) { + return subview; + } + } + } + + return nil; +} + +- (void)qmuibdg_layoutSubviews { + + void (^layoutBlock)(UIView *view, UIView *badgeView) = ^void(UIView *view, UIView *badgeView) { + BeginIgnoreDeprecatedWarning + CGPoint offset = badgeView == view.qmui_badgeView + ? (IS_LANDSCAPE ? view.qmui_badgeOffsetLandscape : view.qmui_badgeOffset) + : (IS_LANDSCAPE ? view.qmui_updatesIndicatorOffsetLandscape : view.qmui_updatesIndicatorOffset); + EndIgnoreDeprecatedWarning + + UIView *contentView = [view findBarButtonContentView]; + if (contentView) { + CGRect imageViewFrame = [view convertRect:contentView.frame fromView:contentView.superview]; + badgeView.frame = CGRectSetXY(badgeView.frame, CGRectGetMaxX(imageViewFrame) + offset.x, CGRectGetMinY(imageViewFrame) - CGRectGetHeight(badgeView.frame) + offset.y); + } else { + badgeView.frame = CGRectSetXY(badgeView.frame, CGRectGetWidth(view.bounds) + offset.x, - CGRectGetHeight(badgeView.frame) + offset.y); + } + [view bringSubviewToFront:badgeView]; + }; + + if (self.qmui_updatesIndicatorView && !self.qmui_updatesIndicatorView.hidden) { + layoutBlock(self, self.qmui_updatesIndicatorView); + if (self.qmui_updatesIndicatorViewDidLayoutBlock) { + self.qmui_updatesIndicatorViewDidLayoutBlock(self, self.qmui_updatesIndicatorView); + } + } + if (self.qmui_badgeView && !self.qmui_badgeView.hidden) { + [self.qmui_badgeView sizeToFit]; + layoutBlock(self, self.qmui_badgeView); + if (self.qmui_badgeViewDidLayoutBlock) { + self.qmui_badgeViewDidLayoutBlock(self, self.qmui_badgeView); + } + } +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.h b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.h index bc7d4ad7..88f9ed47 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIButton.h @@ -25,14 +25,20 @@ typedef NS_ENUM(NSUInteger, QMUIButtonImagePosition) { QMUIButtonImagePositionRight, // imageView在titleLabel右边 }; +/** + * 用于 `QMUIButton.cornerRadius` 属性,当 `cornerRadius` 为 `QMUIButtonCornerRadiusAdjustsBounds` 时,`QMUIButton` 会在高度变化时自动调整 `cornerRadius`,使其始终保持为高度的 1/2。 + */ +extern const CGFloat QMUIButtonCornerRadiusAdjustsBounds; + /** * 提供以下功能: - * 1. 支持让文字和图片自动跟随 tintColor 变化(系统的 UIButton 默认是不响应 tintColor 的) - * 2. highlighted、disabled 状态均通过改变整个按钮的alpha来表现,无需分别设置不同 state 下的 titleColor、image。alpha 的值可在配置表里修改 ButtonHighlightedAlpha、ButtonDisabledAlpha。 - * 3. 支持点击时改变背景色颜色(highlightedBackgroundColor) - * 4. 支持点击时改变边框颜色(highlightedBorderColor) - * 5. 支持设置图片相对于 titleLabel 的位置(imagePosition) - * 6. 支持设置图片和 titleLabel 之间的间距,无需自行调整 titleEdgeInests、imageEdgeInsets(spacingBetweenImageAndTitle) + * 1. 支持让文字和图片自动跟随 tintColor 变化(系统的 UIButton 默认是不响应 tintColor 的)。 + * 2. 支持自动将圆角值保持为按钮高度的一半。 + * 3. highlighted、disabled 状态均通过改变整个按钮的alpha来表现,无需分别设置不同 state 下的 titleColor、image。alpha 的值可在配置表里修改 ButtonHighlightedAlpha、ButtonDisabledAlpha。 + * 4. 支持点击时改变背景色颜色(highlightedBackgroundColor)。 + * 5. 支持点击时改变边框颜色(highlightedBorderColor)。 + * 6. 支持设置图片相对于 titleLabel 的位置(imagePosition)。 + * 7. 支持设置图片和 titleLabel 之间的间距,无需自行调整 titleEdgeInests、imageEdgeInsets(spacingBetweenImageAndTitle)。 * @warning QMUIButton 重新定义了 UIButton.titleEdgeInests、imageEdgeInsets、contentEdgeInsets 这三者的布局逻辑,sizeThatFits: 里会把 titleEdgeInests 和 imageEdgeInsets 也考虑在内(UIButton 不会),以使这三个接口的使用更符合直觉。 */ @interface QMUIButton : UIButton @@ -42,6 +48,11 @@ typedef NS_ENUM(NSUInteger, QMUIButtonImagePosition) { */ - (void)didInitialize NS_REQUIRES_SUPER; +@property(nonatomic, strong, nullable) NSString *subtitle; +@property(nonatomic, strong, readonly) UILabel *subtitleLabel; +@property(nonatomic, assign) IBInspectable UIEdgeInsets subtitleEdgeInsets; +@property(nonatomic, strong, nullable) IBInspectable UIColor *subtitleColor; + /** * 让按钮的文字颜色自动跟随tintColor调整(系统默认titleColor是不跟随的)
* 默认为NO @@ -56,7 +67,6 @@ typedef NS_ENUM(NSUInteger, QMUIButtonImagePosition) { /** * 等价于 adjustsTitleTintColorAutomatically = YES & adjustsImageTintColorAutomatically = YES & tintColor = xxx - * @note 一般只使用这个属性的 setter,而 getter 永远返回 self.tintColor * @warning 不支持传 nil */ @property(nonatomic, strong) IBInspectable UIColor *tintColorAdjustsTitleAndImage; @@ -100,6 +110,8 @@ typedef NS_ENUM(NSUInteger, QMUIButtonImagePosition) { */ @property(nonatomic, assign) IBInspectable CGFloat spacingBetweenImageAndTitle; +@property(nonatomic, assign) IBInspectable CGFloat cornerRadius UI_APPEARANCE_SELECTOR;// 默认为 0。将其设置为 QMUIButtonCornerRadiusAdjustsBounds 可自动保持圆角为按钮高度的一半。 + @end NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m index dc800455..28944c00 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIButton.m @@ -15,9 +15,11 @@ #import "QMUIButton.h" #import "QMUICore.h" -#import "QMUILog.h" #import "CALayer+QMUI.h" #import "UIButton+QMUI.h" +#import "QMUILayouter.h" + +const CGFloat QMUIButtonCornerRadiusAdjustsBounds = -1; @interface QMUIButton () @@ -27,17 +29,19 @@ @interface QMUIButton () @implementation QMUIButton +@synthesize subtitleLabel = _qmuisubtitleLabel; + - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { - [self didInitialize]; - self.tintColor = ButtonTintColor; - if (!self.adjustsTitleTintColorAutomatically) { - [self setTitleColor:self.tintColor forState:UIControlStateNormal]; - } + [self setTitleColor:self.tintColor forState:UIControlStateNormal];// 初始化时 adjustsTitleTintColorAutomatically 还是 NO,所以这里手动把 titleColor 设置为 tintColor 的值 + self.subtitleColor = self.tintColor; // iOS7以后的button,sizeToFit后默认会自带一个上下的contentInsets,为了保证按钮大小即为内容大小,这里直接去掉,改为一个最小的值。 self.contentEdgeInsets = UIEdgeInsetsMake(CGFLOAT_MIN, 0, CGFLOAT_MIN, 0); + + // 放在后面,让前面的默认值可以被子类重写的 didInitialize 覆盖 + [self didInitialize]; } return self; } @@ -50,9 +54,6 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { } - (void)didInitialize { - self.adjustsTitleTintColorAutomatically = NO; - self.adjustsImageTintColorAutomatically = NO; - // 默认接管highlighted和disabled的表现,去掉系统默认的表现 self.adjustsImageWhenHighlighted = NO; self.adjustsImageWhenDisabled = NO; @@ -61,73 +62,153 @@ - (void)didInitialize { // 图片默认在按钮左边,与系统UIButton保持一致 self.imagePosition = QMUIButtonImagePositionLeft; + + _qmuisubtitleLabel = [[UILabel alloc] init]; + _qmuisubtitleLabel.textColor = self.subtitleColor; + _qmuisubtitleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; + + self.subtitleEdgeInsets = UIEdgeInsetsMake(4, 0, 0, 0); } -- (CGSize)sizeThatFits:(CGSize)size { - // 如果调用 sizeToFit,那么传进来的 size 就是当前按钮的 size,此时的计算不要去限制宽高 - if (CGSizeEqualToSize(self.bounds.size, size)) { - size = CGSizeMax; +- (void)setSubtitle:(NSString *)subtitle { + _subtitle = subtitle; + if (subtitle.length) { + [self addSubview:_qmuisubtitleLabel]; + _qmuisubtitleLabel.text = subtitle; + } else { + [_qmuisubtitleLabel removeFromSuperview]; } + [self setNeedsLayout]; +} + +- (void)setSubtitleEdgeInsets:(UIEdgeInsets)subtitleEdgeInsets { + _subtitleEdgeInsets = subtitleEdgeInsets; + [self setNeedsLayout]; +} + +- (void)setSubtitleColor:(UIColor *)subtitleColor { + _subtitleColor = subtitleColor; + _qmuisubtitleLabel.textColor = subtitleColor; +} + +// 系统访问 self.imageView 会触发 layout,而私有方法 _imageView 则是简单地访问 imageView,所以在 QMUIButton layoutSubviews 里应该用这个方法 +// https://github.com/Tencent/QMUI_iOS/issues/1051 +- (UIImageView *)_qmui_imageView { + BeginIgnorePerformSelectorLeaksWarning + return [self performSelector:NSSelectorFromString(@"_imageView")]; + EndIgnorePerformSelectorLeaksWarning +} + +- (QMUILayouterItem *)generateLayouterForLayout:(BOOL)forLayout { + __weak __typeof(self)weakSelf = self; + + QMUILayouterAlignment horizontal = [@[ + @(QMUILayouterAlignmentCenter), + @(QMUILayouterAlignmentLeading), + @(QMUILayouterAlignmentTrailing), + @(QMUILayouterAlignmentFill), + @(QMUILayouterAlignmentLeading), + @(QMUILayouterAlignmentTrailing), + ][self.contentHorizontalAlignment] integerValue]; + QMUILayouterAlignment vertical = [@[ + @(QMUILayouterAlignmentCenter), + @(QMUILayouterAlignmentLeading), + @(QMUILayouterAlignmentTrailing), + @(QMUILayouterAlignmentFill), + ][self.contentVerticalAlignment] integerValue]; BOOL isImageViewShowing = !!self.currentImage; - BOOL isTitleLabelShowing = !!self.currentTitle || self.currentAttributedTitle; - CGSize imageTotalSize = CGSizeZero;// 包含 imageEdgeInsets 那些空间 - CGSize titleTotalSize = CGSizeZero;// 包含 titleEdgeInsets 那些空间 - CGFloat spacingBetweenImageAndTitle = flat(isImageViewShowing && isTitleLabelShowing ? self.spacingBetweenImageAndTitle : 0);// 如果图片或文字某一者没显示,则这个 spacing 不考虑进布局 - UIEdgeInsets contentEdgeInsets = UIEdgeInsetsRemoveFloatMin(self.contentEdgeInsets); - CGSize resultSize = CGSizeZero; - CGSize contentLimitSize = CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(contentEdgeInsets), size.height - UIEdgeInsetsGetVerticalValue(contentEdgeInsets)); + QMUILayouterItem *image = [QMUILayouterItem itemWithView:isImageViewShowing ? (forLayout ? self._qmui_imageView : self.imageView) : nil margin:self.imageEdgeInsets]; + image.visibleBlock = ^BOOL(QMUILayouterItem * _Nonnull aItem) { + return !!weakSelf.currentImage; + }; + image.sizeThatFitsBlock = ^CGSize(QMUILayouterItem * _Nonnull aItem, CGSize size, CGSize superResult) { + // 某些时机下存在 image 但 imageView.image 尚为 nil 导致计算出来的尺寸错误,所以这里做个保护(ed4d87e86af12110b2c14359ef287be959c70af0) + if (aItem.visible && CGSizeIsEmpty(superResult) && [aItem.view.superview isKindOfClass:QMUIButton.class]) { + QMUIButton *btn = (QMUIButton *)aItem.view.superview; + return btn.currentImage.size; + } + return superResult; + }; + QMUILayouterItem *title = [QMUILayouterItem itemWithView:self.titleLabel margin:self.titleEdgeInsets grow:QMUILayouterGrowNever shrink:QMUILayouterShrinkDefault]; + title.visibleBlock = ^BOOL(QMUILayouterItem * _Nonnull aItem) { + return !!weakSelf.currentTitle || !!weakSelf.currentAttributedTitle; + }; + QMUILayouterItem *subtitle = [QMUILayouterItem itemWithView:self.subtitleLabel margin:self.subtitleEdgeInsets grow:QMUILayouterGrowNever shrink:QMUILayouterShrinkDefault]; + QMUILayouterLinearVertical *titles = [QMUILayouterLinearVertical itemWithChildItems:@[ + title, + subtitle, + ] spacingBetweenItems:0 horizontal:horizontal vertical:vertical]; + titles.shrink = QMUILayouterShrinkDefault; - switch (self.imagePosition) { - case QMUIButtonImagePositionTop: - case QMUIButtonImagePositionBottom: { - // 图片和文字上下排版时,宽度以文字或图片的最大宽度为最终宽度 - if (isImageViewShowing) { - CGFloat imageLimitWidth = contentLimitSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets); - CGSize imageSize = self.imageView.image ? [self.imageView sizeThatFits:CGSizeMake(imageLimitWidth, CGFLOAT_MAX)] : self.currentImage.size; - imageSize.width = fmin(imageSize.width, imageLimitWidth); - imageTotalSize = CGSizeMake(imageSize.width + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets), imageSize.height + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets)); - } - - if (isTitleLabelShowing) { - CGSize titleLimitSize = CGSizeMake(contentLimitSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), contentLimitSize.height - imageTotalSize.height - spacingBetweenImageAndTitle - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize]; - titleSize.height = fmin(titleSize.height, titleLimitSize.height); - titleTotalSize = CGSizeMake(titleSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), titleSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); + if (self.imagePosition == QMUIButtonImagePositionTop || self.imagePosition == QMUIButtonImagePositionBottom) { + if (vertical == QMUILayouterAlignmentFill) { + if (image.visible && title.visible && !subtitle.visible) { + titles.grow = QMUILayouterGrowMost; + title.grow = QMUILayouterGrowMost; + } else if (image.visible && !title.visible && subtitle.visible) { + titles.grow = QMUILayouterGrowMost; + subtitle.grow = QMUILayouterGrowMost; + } else if (!image.visible && title.visible && subtitle.visible) { + titles.grow = QMUILayouterGrowMost; + title.grow = QMUILayouterGrowMost; } - - resultSize.width = UIEdgeInsetsGetHorizontalValue(contentEdgeInsets); - resultSize.width += fmax(imageTotalSize.width, titleTotalSize.width); - resultSize.height = UIEdgeInsetsGetVerticalValue(contentEdgeInsets) + imageTotalSize.height + spacingBetweenImageAndTitle + titleTotalSize.height; } - break; - - case QMUIButtonImagePositionLeft: - case QMUIButtonImagePositionRight: { - // 图片和文字水平排版时,高度以文字或图片的最大高度为最终高度 - // 注意这里有一个和系统不一致的行为:当 titleLabel 为多行时,系统的 sizeThatFits: 计算结果固定是单行的,所以当 QMUIButtonImagePositionLeft 并且titleLabel 多行的情况下,QMUIButton 计算的结果与系统不一致 - - if (isImageViewShowing) { - CGFloat imageLimitHeight = contentLimitSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets); - CGSize imageSize = self.imageView.image ? [self.imageView sizeThatFits:CGSizeMake(CGFLOAT_MAX, imageLimitHeight)] : self.currentImage.size; - imageSize.height = fmin(imageSize.height, imageLimitHeight); - imageTotalSize = CGSizeMake(imageSize.width + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets), imageSize.height + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets)); + } else if (self.imagePosition == QMUIButtonImagePositionLeft || self.imagePosition == QMUIButtonImagePositionRight) { + if (horizontal == QMUILayouterAlignmentFill) { + if (image.visible && (title.visible || subtitle.visible)) { + titles.grow = QMUILayouterGrowMost; } - - if (isTitleLabelShowing) { - CGSize titleLimitSize = CGSizeMake(contentLimitSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets) - imageTotalSize.width - spacingBetweenImageAndTitle, contentLimitSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize]; - titleSize.height = fmin(titleSize.height, titleLimitSize.height); - titleTotalSize = CGSizeMake(titleSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), titleSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); + } + if (vertical == QMUILayouterAlignmentFill) { + if (title.visible) { + title.grow = QMUILayouterGrowMost; + } else if (subtitle.visible) { + subtitle.grow = QMUILayouterGrowMost; } - - resultSize.width = UIEdgeInsetsGetHorizontalValue(contentEdgeInsets) + imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width; - resultSize.height = UIEdgeInsetsGetVerticalValue(contentEdgeInsets); - resultSize.height += fmax(imageTotalSize.height, titleTotalSize.height); } - break; } - return resultSize; + + switch (self.imagePosition) { + case QMUIButtonImagePositionTop: { + return [QMUILayouterLinearVertical itemWithChildItems:@[ + image, + titles, + ] spacingBetweenItems:self.spacingBetweenImageAndTitle horizontal:horizontal vertical:vertical]; + } + case QMUIButtonImagePositionBottom: { + return [QMUILayouterLinearVertical itemWithChildItems:@[ + titles, + image, + ] spacingBetweenItems:self.spacingBetweenImageAndTitle horizontal:horizontal vertical:vertical]; + } + case QMUIButtonImagePositionLeft: { + return [QMUILayouterLinearHorizontal itemWithChildItems:@[ + image, + titles, + ] spacingBetweenItems:self.spacingBetweenImageAndTitle horizontal:horizontal vertical:vertical]; + } + case QMUIButtonImagePositionRight: { + return [QMUILayouterLinearHorizontal itemWithChildItems:@[ + titles, + image, + ] spacingBetweenItems:self.spacingBetweenImageAndTitle horizontal:horizontal vertical:vertical]; + } + } +} + +- (CGSize)sizeThatFits:(CGSize)size { + // 如果调用 sizeToFit,那么传进来的 size 就是当前按钮的 size,此时的计算不要去限制宽高 + // 系统 UIButton 不管任何时候,对 sizeThatFits:CGSizeZero 都会返回真实的内容大小,这里对齐 + if (CGSizeEqualToSize(self.bounds.size, size) || CGSizeIsEmpty(size)) { + size = CGSizeMax; + } + + QMUILayouterItem *layouter = [self generateLayouterForLayout:NO]; + CGSize result = [layouter sizeThatFits:size]; + result.width += UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets); + result.height += UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets); + return result; } - (CGSize)intrinsicContentSize { @@ -141,277 +222,29 @@ - (void)layoutSubviews { return; } - BOOL isImageViewShowing = !!self.currentImage; - BOOL isTitleLabelShowing = !!self.currentTitle || !!self.currentAttributedTitle; - CGSize imageLimitSize = CGSizeZero; - CGSize titleLimitSize = CGSizeZero; - CGSize imageTotalSize = CGSizeZero;// 包含 imageEdgeInsets 那些空间 - CGSize titleTotalSize = CGSizeZero;// 包含 titleEdgeInsets 那些空间 - CGFloat spacingBetweenImageAndTitle = flat(isImageViewShowing && isTitleLabelShowing ? self.spacingBetweenImageAndTitle : 0);// 如果图片或文字某一者没显示,则这个 spacing 不考虑进布局 - CGRect imageFrame = CGRectZero; - CGRect titleFrame = CGRectZero; - UIEdgeInsets contentEdgeInsets = UIEdgeInsetsRemoveFloatMin(self.contentEdgeInsets); - CGSize contentSize = CGSizeMake(CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(contentEdgeInsets), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(contentEdgeInsets)); - - // 图片的布局原则都是尽量完整展示,所以不管 imagePosition 的值是什么,这个计算过程都是相同的 - if (isImageViewShowing) { - imageLimitSize = CGSizeMake(contentSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets), contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets)); - CGSize imageSize = self.imageView.image ? [self.imageView sizeThatFits:imageLimitSize] : self.currentImage.size; - imageSize.width = fmin(imageLimitSize.width, imageSize.width); - imageSize.height = fmin(imageLimitSize.height, imageSize.height); - imageFrame = CGRectMakeWithSize(imageSize); - imageTotalSize = CGSizeMake(imageSize.width + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets), imageSize.height + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets)); + if (self.cornerRadius == QMUIButtonCornerRadiusAdjustsBounds) { + self.layer.cornerRadius = CGRectGetHeight(self.bounds) / 2; } - if (self.imagePosition == QMUIButtonImagePositionTop || self.imagePosition == QMUIButtonImagePositionBottom) { - - if (isTitleLabelShowing) { - titleLimitSize = CGSizeMake(contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), contentSize.height - imageTotalSize.height - spacingBetweenImageAndTitle - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize]; - titleSize.width = fmin(titleLimitSize.width, titleSize.width); - titleSize.height = fmin(titleLimitSize.height, titleSize.height); - titleFrame = CGRectMakeWithSize(titleSize); - titleTotalSize = CGSizeMake(titleSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), titleSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - } - - switch (self.contentHorizontalAlignment) { - case UIControlContentHorizontalAlignmentLeft: - imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left) : titleFrame; - break; - case UIControlContentHorizontalAlignmentCenter: - imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left + CGFloatGetCenter(imageLimitSize.width, CGRectGetWidth(imageFrame))) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left + CGFloatGetCenter(titleLimitSize.width, CGRectGetWidth(titleFrame))) : titleFrame; - break; - case UIControlContentHorizontalAlignmentRight: - imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame)) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.titleEdgeInsets.right - CGRectGetWidth(titleFrame)) : titleFrame; - break; - case UIControlContentHorizontalAlignmentFill: - if (isImageViewShowing) { - imageFrame = CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left); - imageFrame = CGRectSetWidth(imageFrame, imageLimitSize.width); - } - if (isTitleLabelShowing) { - titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left); - titleFrame = CGRectSetWidth(titleFrame, titleLimitSize.width); - } - break; - default: - break; - } - - if (self.imagePosition == QMUIButtonImagePositionTop) { - switch (self.contentVerticalAlignment) { - case UIControlContentVerticalAlignmentTop: - imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + imageTotalSize.height + spacingBetweenImageAndTitle + self.titleEdgeInsets.top) : titleFrame; - break; - case UIControlContentVerticalAlignmentCenter: { - CGFloat contentHeight = imageTotalSize.height + spacingBetweenImageAndTitle + titleTotalSize.height; - CGFloat minY = CGFloatGetCenter(contentSize.height, contentHeight) + contentEdgeInsets.top; - imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, minY + self.imageEdgeInsets.top) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, minY + imageTotalSize.height + spacingBetweenImageAndTitle + self.titleEdgeInsets.top) : titleFrame; - } - break; - case UIControlContentVerticalAlignmentBottom: - titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.titleEdgeInsets.bottom - CGRectGetHeight(titleFrame)) : titleFrame; - imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - titleTotalSize.height - spacingBetweenImageAndTitle - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame)) : imageFrame; - break; - case UIControlContentVerticalAlignmentFill: { - if (isImageViewShowing && isTitleLabelShowing) { - - // 同时显示图片和 label 的情况下,图片高度按本身大小显示,剩余空间留给 label - imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + imageTotalSize.height + spacingBetweenImageAndTitle + self.titleEdgeInsets.top) : titleFrame; - titleFrame = isTitleLabelShowing ? CGRectSetHeight(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.titleEdgeInsets.bottom - CGRectGetMinY(titleFrame)) : titleFrame; - - } else if (isImageViewShowing) { - imageFrame = CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top); - imageFrame = CGRectSetHeight(imageFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets)); - } else { - titleFrame = CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top); - titleFrame = CGRectSetHeight(titleFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - } - } - break; - } - } else { - switch (self.contentVerticalAlignment) { - case UIControlContentVerticalAlignmentTop: - titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top) : titleFrame; - imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + titleTotalSize.height + spacingBetweenImageAndTitle + self.imageEdgeInsets.top) : imageFrame; - break; - case UIControlContentVerticalAlignmentCenter: { - CGFloat contentHeight = imageTotalSize.height + titleTotalSize.height + spacingBetweenImageAndTitle; - CGFloat minY = CGFloatGetCenter(contentSize.height, contentHeight) + contentEdgeInsets.top; - titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, minY + self.titleEdgeInsets.top) : titleFrame; - imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, minY + titleTotalSize.height + spacingBetweenImageAndTitle + self.imageEdgeInsets.top) : imageFrame; - } - break; - case UIControlContentVerticalAlignmentBottom: - imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame)) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - imageTotalSize.height - spacingBetweenImageAndTitle - self.titleEdgeInsets.bottom - CGRectGetHeight(titleFrame)) : titleFrame; - break; - case UIControlContentVerticalAlignmentFill: { - if (isImageViewShowing && isTitleLabelShowing) { - - // 同时显示图片和 label 的情况下,图片高度按本身大小显示,剩余空间留给 label - imageFrame = CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame)); - titleFrame = CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top); - titleFrame = CGRectSetHeight(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - imageTotalSize.height - spacingBetweenImageAndTitle - self.titleEdgeInsets.bottom - CGRectGetMinY(titleFrame)); - - } else if (isImageViewShowing) { - imageFrame = CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top); - imageFrame = CGRectSetHeight(imageFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets)); - } else { - titleFrame = CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top); - titleFrame = CGRectSetHeight(titleFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - } - } - break; - } - } - - if (isImageViewShowing) { - self.imageView.frame = CGRectFlatted(imageFrame); - } - if (isTitleLabelShowing) { - self.titleLabel.frame = CGRectFlatted(titleFrame); - } - - } else if (self.imagePosition == QMUIButtonImagePositionLeft || self.imagePosition == QMUIButtonImagePositionRight) { - - if (isTitleLabelShowing) { - titleLimitSize = CGSizeMake(contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets) - imageTotalSize.width - spacingBetweenImageAndTitle, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize]; - titleSize.width = fmin(titleLimitSize.width, titleSize.width); - titleSize.height = fmin(titleLimitSize.height, titleSize.height); - titleFrame = CGRectMakeWithSize(titleSize); - titleTotalSize = CGSizeMake(titleSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), titleSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - } - - switch (self.contentVerticalAlignment) { - case UIControlContentVerticalAlignmentTop: - imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top) : titleFrame; - - break; - case UIControlContentVerticalAlignmentCenter: - imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + CGFloatGetCenter(contentSize.height, CGRectGetHeight(imageFrame)) + self.imageEdgeInsets.top) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + CGFloatGetCenter(contentSize.height, CGRectGetHeight(titleFrame)) + self.titleEdgeInsets.top) : titleFrame; - break; - case UIControlContentVerticalAlignmentBottom: - imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame)) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.titleEdgeInsets.bottom - CGRectGetHeight(titleFrame)) : titleFrame; - break; - case UIControlContentVerticalAlignmentFill: - if (isImageViewShowing) { - imageFrame = CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top); - imageFrame = CGRectSetHeight(imageFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets)); - } - if (isTitleLabelShowing) { - titleFrame = CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top); - titleFrame = CGRectSetHeight(titleFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - } - break; - } - - if (self.imagePosition == QMUIButtonImagePositionLeft) { - switch (self.contentHorizontalAlignment) { - case UIControlContentHorizontalAlignmentLeft: - imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + imageTotalSize.width + spacingBetweenImageAndTitle + self.titleEdgeInsets.left) : titleFrame; - break; - case UIControlContentHorizontalAlignmentCenter: { - CGFloat contentWidth = imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width; - CGFloat minX = contentEdgeInsets.left + CGFloatGetCenter(contentSize.width, contentWidth); - imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, minX + self.imageEdgeInsets.left) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, minX + imageTotalSize.width + spacingBetweenImageAndTitle + self.titleEdgeInsets.left) : titleFrame; - } - break; - case UIControlContentHorizontalAlignmentRight: { - if (imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width > contentSize.width) { - // 图片和文字总宽超过按钮宽度,则优先完整显示图片 - imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + imageTotalSize.width + spacingBetweenImageAndTitle + self.titleEdgeInsets.left) : titleFrame; - } else { - // 内容不超过按钮宽度,则靠右布局即可 - titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.titleEdgeInsets.right - CGRectGetWidth(titleFrame)) : titleFrame; - imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - titleTotalSize.width - spacingBetweenImageAndTitle - imageTotalSize.width + self.imageEdgeInsets.left) : imageFrame; - } - } - break; - case UIControlContentHorizontalAlignmentFill: { - if (isImageViewShowing && isTitleLabelShowing) { - // 同时显示图片和 label 的情况下,图片按本身宽度显示,剩余空间留给 label - imageFrame = CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left); - titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + imageTotalSize.width + spacingBetweenImageAndTitle + self.titleEdgeInsets.left); - titleFrame = CGRectSetWidth(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.titleEdgeInsets.right - CGRectGetMinX(titleFrame)); - } else if (isImageViewShowing) { - imageFrame = CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left); - imageFrame = CGRectSetWidth(imageFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets)); - } else { - titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left); - titleFrame = CGRectSetWidth(titleFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets)); - } - } - break; - default: - break; - } - } else { - switch (self.contentHorizontalAlignment) { - case UIControlContentHorizontalAlignmentLeft: { - if (imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width > contentSize.width) { - // 图片和文字总宽超过按钮宽度,则优先完整显示图片 - imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame)) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - imageTotalSize.width - spacingBetweenImageAndTitle - titleTotalSize.width + self.titleEdgeInsets.left) : titleFrame; - } else { - // 内容不超过按钮宽度,则靠左布局即可 - titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left) : titleFrame; - imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + titleTotalSize.width + spacingBetweenImageAndTitle + self.imageEdgeInsets.left) : imageFrame; - } - } - break; - case UIControlContentHorizontalAlignmentCenter: { - CGFloat contentWidth = imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width; - CGFloat minX = contentEdgeInsets.left + CGFloatGetCenter(contentSize.width, contentWidth); - titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, minX + self.titleEdgeInsets.left) : titleFrame; - imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, minX + titleTotalSize.width + spacingBetweenImageAndTitle + self.imageEdgeInsets.left) : imageFrame; - } - break; - case UIControlContentHorizontalAlignmentRight: - imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame)) : imageFrame; - titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - imageTotalSize.width - spacingBetweenImageAndTitle - self.titleEdgeInsets.right - CGRectGetWidth(titleFrame)) : titleFrame; - break; - case UIControlContentHorizontalAlignmentFill: { - if (isImageViewShowing && isTitleLabelShowing) { - // 图片按自身大小显示,剩余空间由标题占满 - imageFrame = CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame)); - titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left); - titleFrame = CGRectSetWidth(titleFrame, CGRectGetMinX(imageFrame) - self.imageEdgeInsets.left - spacingBetweenImageAndTitle - self.titleEdgeInsets.right - CGRectGetMinX(titleFrame)); - - } else if (isImageViewShowing) { - imageFrame = CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left); - imageFrame = CGRectSetWidth(imageFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets)); - } else { - titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left); - titleFrame = CGRectSetWidth(titleFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets)); - } - } - break; - default: - break; - } - } - - if (isImageViewShowing) { - self.imageView.frame = CGRectFlatted(imageFrame); + QMUILayouterItem *layouter = [self generateLayouterForLayout:YES]; + layouter.frame = CGRectInsetEdges(self.bounds, self.contentEdgeInsets); + [layouter layoutIfNeeded]; + + // UIButton 有一个特性是不管哪种 alignment,imageView 的宽高必定不超过 button 的宽高(也不管 imageView 的宽高比例是否产生变化),从而保证就算设置了超过 button 大小的 image,也会在 button 容器内部显示。这里对齐系统的特性 + BOOL isImageViewShowing = !!self.currentImage; + if (isImageViewShowing && !CGRectIsEmpty(self.bounds)) { + UIImageView *imageView = self._qmui_imageView; + CGRect rect = imageView.frame; + CGRect limitRect = CGRectInsetEdges(CGRectInsetEdges(self.bounds, self.contentEdgeInsets), self.imageEdgeInsets); + if (CGRectGetWidth(rect) > CGRectGetWidth(limitRect)) { + rect = CGRectSetWidth(rect, CGRectGetWidth(limitRect)); + rect = CGRectSetX(rect, self.contentEdgeInsets.left + self.imageEdgeInsets.left); } - if (isTitleLabelShowing) { - self.titleLabel.frame = CGRectFlatted(titleFrame); + if (CGRectGetHeight(rect) > CGRectGetHeight(limitRect)) { + rect = CGRectSetHeight(rect, CGRectGetHeight(limitRect)); + rect = CGRectSetY(rect, self.contentEdgeInsets.top + self.imageEdgeInsets.top); } + imageView.frame = rect; } } @@ -471,10 +304,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; } } @@ -487,6 +318,7 @@ - (void)adjustsButtonHighlighted { } self.highlightedBackgroundLayer.frame = self.bounds; self.highlightedBackgroundLayer.cornerRadius = self.layer.cornerRadius; + self.highlightedBackgroundLayer.maskedCorners = self.layer.maskedCorners; self.highlightedBackgroundLayer.backgroundColor = self.highlighted ? self.highlightedBackgroundColor.CGColor : UIColorClear.CGColor; } @@ -501,14 +333,16 @@ - (void)setAdjustsTitleTintColorAutomatically:(BOOL)adjustsTitleTintColorAutomat } - (void)updateTitleColorIfNeeded { - if (self.adjustsTitleTintColorAutomatically && self.currentTitleColor) { + if (!self.adjustsTitleTintColorAutomatically) return; + if (self.currentTitleColor) { [self setTitleColor:self.tintColor forState:UIControlStateNormal]; } - if (self.adjustsTitleTintColorAutomatically && self.currentAttributedTitle) { + if (self.currentAttributedTitle) { NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:self.currentAttributedTitle]; [attributedString addAttribute:NSForegroundColorAttributeName value:self.tintColor range:NSMakeRange(0, attributedString.length)]; [self setAttributedTitle:attributedString forState:UIControlStateNormal]; } + self.subtitleColor = self.tintColor; } - (void)setAdjustsImageTintColorAutomatically:(BOOL)adjustsImageTintColorAutomatically { @@ -527,7 +361,10 @@ - (void)updateImageRenderingModeIfNeeded { for (NSNumber *number in states) { UIImage *image = [self imageForState:number.unsignedIntegerValue]; if (!image) { - return; + continue; + } + if (number.unsignedIntegerValue != UIControlStateNormal && image == [self imageForState:UIControlStateNormal]) { + continue; } if (self.adjustsImageTintColorAutomatically) { @@ -542,7 +379,7 @@ - (void)updateImageRenderingModeIfNeeded { } - (void)setImage:(UIImage *)image forState:(UIControlState)state { - if (self.adjustsImageTintColorAutomatically) { + if (self.adjustsImageTintColorAutomatically && image.renderingMode != UIImageRenderingModeAlwaysOriginal) { image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; } @@ -560,6 +397,7 @@ - (void)tintColorDidChange { } - (void)setTintColorAdjustsTitleAndImage:(UIColor *)tintColorAdjustsTitleAndImage { + _tintColorAdjustsTitleAndImage = tintColorAdjustsTitleAndImage; if (tintColorAdjustsTitleAndImage) { self.tintColor = tintColorAdjustsTitleAndImage; self.adjustsTitleTintColorAutomatically = YES; @@ -567,8 +405,12 @@ - (void)setTintColorAdjustsTitleAndImage:(UIColor *)tintColorAdjustsTitleAndImag } } -- (UIColor *)tintColorAdjustsTitleAndImage { - return self.tintColor; +- (void)setCornerRadius:(CGFloat)cornerRadius { + _cornerRadius = cornerRadius; + if (cornerRadius != QMUIButtonCornerRadiusAdjustsBounds) { + self.layer.cornerRadius = cornerRadius; + } + [self setNeedsLayout]; } @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIFillButton.h b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIFillButton.h deleted file mode 100644 index f6b84432..00000000 --- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIFillButton.h +++ /dev/null @@ -1,55 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUIFillButton.h -// QMUIKit -// -// Created by QMUI Team on 2018/4/9. -// - -#import "QMUIButton.h" - -NS_ASSUME_NONNULL_BEGIN - -typedef NS_ENUM(NSUInteger, QMUIFillButtonColor) { - QMUIFillButtonColorBlue, - QMUIFillButtonColorRed, - QMUIFillButtonColorGreen, - QMUIFillButtonColorGray, - QMUIFillButtonColorWhite, -}; - -/** - * 用于 `QMUIFillButton.cornerRadius` 属性,当 `cornerRadius` 为 `QMUIFillButtonCornerRadiusAdjustsBounds` 时,`QMUIFillButton` 会在高度变化时自动调整 `cornerRadius`,使其始终保持为高度的 1/2。 - */ -extern const CGFloat QMUIFillButtonCornerRadiusAdjustsBounds; - -/** - * QMUIFillButton - * 实心填充颜色的按钮,支持预定义的几个色值 - */ -@interface QMUIFillButton : QMUIButton - -@property(nonatomic, strong, nullable) IBInspectable UIColor *fillColor; // 默认为 FillButtonColorBlue -@property(nonatomic, strong, nullable) IBInspectable UIColor *titleTextColor; // 默认为 UIColorWhite -@property(nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR;// 默认为 QMUIFillButtonCornerRadiusAdjustsBounds,也即固定保持按钮高度的一半。 - -/** - * 控制按钮里面的图片是否也要跟随 `titleTextColor` 一起变化,默认为 `NO` - */ -@property(nonatomic, assign) BOOL adjustsImageWithTitleTextColor UI_APPEARANCE_SELECTOR; - -- (instancetype)initWithFillType:(QMUIFillButtonColor)fillType; -- (instancetype)initWithFillType:(QMUIFillButtonColor)fillType frame:(CGRect)frame; -- (instancetype)initWithFillColor:(nullable UIColor *)fillColor titleTextColor:(nullable UIColor *)textColor; -- (instancetype)initWithFillColor:(nullable UIColor *)fillColor titleTextColor:(nullable UIColor *)textColor frame:(CGRect)frame; - -@end - -NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIFillButton.m b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIFillButton.m deleted file mode 100644 index 83999c21..00000000 --- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIFillButton.m +++ /dev/null @@ -1,163 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUIFillButton.m -// QMUIKit -// -// Created by QMUI Team on 2018/4/9. -// - -#import "QMUIFillButton.h" -#import "QMUICore.h" - -const CGFloat QMUIFillButtonCornerRadiusAdjustsBounds = -1; - -@implementation QMUIFillButton - -- (instancetype)init { - return [self initWithFillType:QMUIFillButtonColorBlue]; -} - -- (instancetype)initWithFrame:(CGRect)frame { - return [self initWithFillType:QMUIFillButtonColorBlue frame:frame]; -} - -- (instancetype)initWithFillType:(QMUIFillButtonColor)fillType { - return [self initWithFillType:fillType frame:CGRectZero]; -} - -- (instancetype)initWithFillType:(QMUIFillButtonColor)fillType frame:(CGRect)frame { - UIColor *fillColor = nil; - UIColor *textColor = UIColorWhite; - switch (fillType) { - case QMUIFillButtonColorBlue: - fillColor = FillButtonColorBlue; - break; - case QMUIFillButtonColorRed: - fillColor = FillButtonColorRed; - break; - case QMUIFillButtonColorGreen: - fillColor = FillButtonColorGreen; - break; - case QMUIFillButtonColorGray: - fillColor = FillButtonColorGray; - break; - case QMUIFillButtonColorWhite: - fillColor = FillButtonColorWhite; - textColor = UIColorBlue; - default: - break; - } - return [self initWithFillColor:fillColor titleTextColor:textColor frame:frame]; -} - -- (instancetype)initWithFillColor:(UIColor *)fillColor titleTextColor:(UIColor *)textColor { - return [self initWithFillColor:fillColor titleTextColor:textColor frame:CGRectZero]; -} - -- (instancetype)initWithFillColor:(UIColor *)fillColor titleTextColor:(UIColor *)textColor frame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - self.fillColor = fillColor; - self.titleTextColor = textColor; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - self.fillColor = FillButtonColorBlue; - self.titleTextColor = UIColorWhite; - } - return self; -} - -- (void)setAdjustsImageWithTitleTextColor:(BOOL)adjustsImageWithTitleTextColor { - _adjustsImageWithTitleTextColor = adjustsImageWithTitleTextColor; - if (adjustsImageWithTitleTextColor) { - [self updateImageColor]; - } -} - -- (void)setFillColor:(UIColor *)fillColor { - _fillColor = fillColor; - self.backgroundColor = fillColor; -} - -- (void)setTitleTextColor:(UIColor *)titleTextColor { - _titleTextColor = titleTextColor; - [self setTitleColor:titleTextColor forState:UIControlStateNormal]; - if (self.adjustsImageWithTitleTextColor) { - [self updateImageColor]; - } -} - -- (void)setImage:(UIImage *)image forState:(UIControlState)state { - if (self.adjustsImageWithTitleTextColor) { - image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - } - [super setImage:image forState:state]; -} - -- (void)updateImageColor { - self.imageView.tintColor = self.adjustsImageWithTitleTextColor ? self.titleTextColor : nil; - if (self.currentImage) { - NSArray *states = @[@(UIControlStateNormal), @(UIControlStateHighlighted), @(UIControlStateDisabled)]; - for (NSNumber *number in states) { - UIImage *image = [self imageForState:[number unsignedIntegerValue]]; - if (!image) { - continue; - } - if (self.adjustsImageWithTitleTextColor) { - // 这里的image不用做renderingMode的处理,而是放到重写的setImage:forState里去做 - [self setImage:image forState:[number unsignedIntegerValue]]; - } else { - // 如果不需要用template的模式渲染,并且之前是使用template的,则把renderingMode改回Original - [self setImage:[image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] forState:[number unsignedIntegerValue]]; - } - } - } -} - -- (void)layoutSublayersOfLayer:(CALayer *)layer { - [super layoutSublayersOfLayer:layer]; - if (self.cornerRadius != QMUIFillButtonCornerRadiusAdjustsBounds) { - self.layer.cornerRadius = self.cornerRadius; - } else { - self.layer.cornerRadius = CGRectGetHeight(self.bounds) / 2; - } -} - -- (void)setCornerRadius:(CGFloat)cornerRadius { - _cornerRadius = cornerRadius; - [self setNeedsLayout]; -} - -@end - -@interface QMUIFillButton (UIAppearance) - -@end - -@implementation QMUIFillButton (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self setDefaultAppearance]; - }); -} - -+ (void)setDefaultAppearance { - QMUIFillButton *appearance = [QMUIFillButton appearance]; - appearance.cornerRadius = QMUIFillButtonCornerRadiusAdjustsBounds; - appearance.adjustsImageWithTitleTextColor = NO; -} - -@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIGhostButton.h b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIGhostButton.h deleted file mode 100644 index 8b6df268..00000000 --- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIGhostButton.h +++ /dev/null @@ -1,56 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUIGhostButton.h -// QMUIKit -// -// Created by QMUI Team on 2018/4/9. -// - -#import "QMUIButton.h" - -NS_ASSUME_NONNULL_BEGIN - -typedef NS_ENUM(NSUInteger, QMUIGhostButtonColor) { - QMUIGhostButtonColorBlue, - QMUIGhostButtonColorRed, - QMUIGhostButtonColorGreen, - QMUIGhostButtonColorGray, - QMUIGhostButtonColorWhite, -}; - -/** - * 用于 `QMUIGhostButton.cornerRadius` 属性,当 `cornerRadius` 为 `QMUIGhostButtonCornerRadiusAdjustsBounds` 时,`QMUIGhostButton` 会在高度变化时自动调整 `cornerRadius`,使其始终保持为高度的 1/2。 - */ -extern const CGFloat QMUIGhostButtonCornerRadiusAdjustsBounds; - -/** - * “幽灵”按钮,也即背景透明、带圆角边框的按钮 - * - * 可通过 `QMUIGhostButtonColor` 设置几种预设的颜色,也可以用 `ghostColor` 设置自定义颜色。 - * - * @warning 默认情况下,`ghostColor` 只会修改文字和边框的颜色,如果需要让 image 也跟随 `ghostColor` 的颜色,则可将 `adjustsImageWithGhostColor` 设为 `YES` - */ -@interface QMUIGhostButton : QMUIButton - -@property(nonatomic, strong, nullable) IBInspectable UIColor *ghostColor; // 默认为 GhostButtonColorBlue -@property(nonatomic, assign) CGFloat borderWidth UI_APPEARANCE_SELECTOR; // 默认为 1pt -@property(nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR; // 默认为 QMUIGhostButtonCornerRadiusAdjustsBounds,也即固定保持按钮高度的一半。 - -/** - * 控制按钮里面的图片是否也要跟随 `ghostColor` 一起变化,默认为 `NO` - */ -@property(nonatomic, assign) BOOL adjustsImageWithGhostColor UI_APPEARANCE_SELECTOR; - -- (instancetype)initWithGhostType:(QMUIGhostButtonColor)ghostType; -- (instancetype)initWithGhostColor:(nullable UIColor *)ghostColor; - -@end - -NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIGhostButton.m b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIGhostButton.m deleted file mode 100644 index a4dc619a..00000000 --- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIGhostButton.m +++ /dev/null @@ -1,160 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUIGhostButton.m -// QMUIKit -// -// Created by QMUI Team on 2018/4/9. -// - -#import "QMUIGhostButton.h" -#import "QMUICore.h" - -const CGFloat QMUIGhostButtonCornerRadiusAdjustsBounds = -1; - -@implementation QMUIGhostButton - -- (instancetype)initWithFrame:(CGRect)frame { - return [self initWithGhostType:QMUIGhostButtonColorBlue frame:frame]; -} - -- (instancetype)initWithGhostType:(QMUIGhostButtonColor)ghostType { - return [self initWithGhostType:ghostType frame:CGRectZero]; -} - -- (instancetype)initWithGhostType:(QMUIGhostButtonColor)ghostType frame:(CGRect)frame { - UIColor *ghostColor = nil; - switch (ghostType) { - case QMUIGhostButtonColorBlue: - ghostColor = GhostButtonColorBlue; - break; - case QMUIGhostButtonColorRed: - ghostColor = GhostButtonColorRed; - break; - case QMUIGhostButtonColorGreen: - ghostColor = GhostButtonColorGreen; - break; - case QMUIGhostButtonColorGray: - ghostColor = GhostButtonColorGray; - break; - case QMUIGhostButtonColorWhite: - ghostColor = GhostButtonColorWhite; - break; - default: - break; - } - return [self initWithGhostColor:ghostColor frame:frame]; -} - -- (instancetype)initWithGhostColor:(UIColor *)ghostColor { - return [self initWithGhostColor:ghostColor frame:CGRectZero]; -} - -- (instancetype)initWithGhostColor:(UIColor *)ghostColor frame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - [self initializeWithGhostColor:ghostColor]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self initializeWithGhostColor:GhostButtonColorBlue]; - } - return self; -} - -- (void)initializeWithGhostColor:(UIColor *)ghostColor { - self.ghostColor = ghostColor; -} - -- (void)setGhostColor:(UIColor *)ghostColor { - _ghostColor = ghostColor; - [self setTitleColor:_ghostColor forState:UIControlStateNormal]; - self.layer.borderColor = _ghostColor.CGColor; - if (self.adjustsImageWithGhostColor) { - [self updateImageColor]; - } -} - -- (void)setBorderWidth:(CGFloat)borderWidth { - _borderWidth = borderWidth; - self.layer.borderWidth = _borderWidth; -} - -- (void)setAdjustsImageWithGhostColor:(BOOL)adjustsImageWithGhostColor { - _adjustsImageWithGhostColor = adjustsImageWithGhostColor; - [self updateImageColor]; -} - -- (void)updateImageColor { - self.imageView.tintColor = self.adjustsImageWithGhostColor ? self.ghostColor : nil; - if (self.currentImage) { - NSArray *states = @[@(UIControlStateNormal), @(UIControlStateHighlighted), @(UIControlStateDisabled)]; - for (NSNumber *number in states) { - UIImage *image = [self imageForState:[number unsignedIntegerValue]]; - if (!image) { - continue; - } - if (self.adjustsImageWithGhostColor) { - // 这里的image不用做renderingMode的处理,而是放到重写的setImage:forState里去做 - [self setImage:image forState:[number unsignedIntegerValue]]; - } else { - // 如果不需要用template的模式渲染,并且之前是使用template的,则把renderingMode改回Original - [self setImage:[image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] forState:[number unsignedIntegerValue]]; - } - } - } -} - -- (void)setImage:(UIImage *)image forState:(UIControlState)state { - if (self.adjustsImageWithGhostColor) { - image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - } - [super setImage:image forState:state]; -} - -- (void)layoutSublayersOfLayer:(CALayer *)layer { - [super layoutSublayersOfLayer:layer]; - if (self.cornerRadius != QMUIGhostButtonCornerRadiusAdjustsBounds) { - self.layer.cornerRadius = self.cornerRadius; - } else { - self.layer.cornerRadius = CGRectGetHeight(self.bounds) / 2; - } -} - -- (void)setCornerRadius:(CGFloat)cornerRadius { - _cornerRadius = cornerRadius; - [self setNeedsLayout]; -} - -@end - -@interface QMUIGhostButton (UIAppearance) - -@end - -@implementation QMUIGhostButton (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self setDefaultAppearance]; - }); -} - -+ (void)setDefaultAppearance { - QMUIGhostButton *appearance = [QMUIGhostButton appearance]; - appearance.borderWidth = 1; - appearance.cornerRadius = QMUIGhostButtonCornerRadiusAdjustsBounds; - appearance.adjustsImageWithGhostColor = NO; -} - -@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUILinkButton.h b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUILinkButton.h deleted file mode 100644 index 7aa32e87..00000000 --- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUILinkButton.h +++ /dev/null @@ -1,39 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUILinkButton.h -// QMUIKit -// -// Created by QMUI Team on 2018/4/9. -// - -#import "QMUIButton.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - * 支持显示下划线的按钮,可用于需要链接的场景。下划线默认和按钮宽度一样,可通过 `underlineInsets` 调整。 - */ -@interface QMUILinkButton : QMUIButton - -/// 控制下划线隐藏或显示,默认为NO,也即显示下划线 -@property(nonatomic, assign) IBInspectable BOOL underlineHidden; - -/// 设置下划线的宽度,默认为 1 -@property(nonatomic, assign) IBInspectable CGFloat underlineWidth; - -/// 控制下划线颜色,若设置为nil,则使用当前按钮的titleColor的颜色作为下划线的颜色。默认为 nil。 -@property(nonatomic, strong, nullable) IBInspectable UIColor *underlineColor; - -/// 下划线的位置是基于 titleLabel 的位置来计算的,默认x、width均和titleLabel一致,而可以通过这个属性来调整下划线的偏移值。默认为UIEdgeInsetsZero。 -@property(nonatomic, assign) UIEdgeInsets underlineInsets; - -@end - -NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUILinkButton.m b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUILinkButton.m deleted file mode 100644 index 02782d3a..00000000 --- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUILinkButton.m +++ /dev/null @@ -1,91 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUILinkButton.m -// QMUIKit -// -// Created by QMUI Team on 2018/4/9. -// - -#import "QMUILinkButton.h" -#import "QMUICore.h" -#import "CALayer+QMUI.h" - -@interface QMUILinkButton () - -@property(nonatomic, strong) CALayer *underlineLayer; -@end - -@implementation QMUILinkButton - -- (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - [self didInitialize]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialize]; - } - return self; -} - -- (void)didInitialize { - [super didInitialize]; - - self.underlineLayer = [CALayer layer]; - [self.underlineLayer qmui_removeDefaultAnimations]; - [self.layer addSublayer:self.underlineLayer]; - - self.underlineHidden = NO; - self.underlineWidth = 1; - self.underlineColor = nil; - self.underlineInsets = UIEdgeInsetsZero; -} - -- (void)setUnderlineHidden:(BOOL)underlineHidden { - _underlineHidden = underlineHidden; - self.underlineLayer.hidden = underlineHidden; -} - -- (void)setUnderlineWidth:(CGFloat)underlineWidth { - _underlineWidth = underlineWidth; - [self setNeedsLayout]; -} - -- (void)setUnderlineColor:(UIColor *)underlineColor { - _underlineColor = underlineColor; - [self updateUnderlineColor]; -} - -- (void)setUnderlineInsets:(UIEdgeInsets)underlineInsets { - _underlineInsets = underlineInsets; - [self setNeedsLayout]; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - if (!self.underlineLayer.hidden) { - self.underlineLayer.frame = CGRectMake(self.underlineInsets.left, CGRectGetMaxY(self.titleLabel.frame) + self.underlineInsets.top - self.underlineInsets.bottom, CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.underlineInsets), self.underlineWidth); - } -} - -- (void)setTitleColor:(UIColor *)color forState:(UIControlState)state { - [super setTitleColor:color forState:state]; - [self updateUnderlineColor]; -} - -- (void)updateUnderlineColor { - UIColor *color = self.underlineColor ? : [self titleColorForState:UIControlStateNormal]; - self.underlineLayer.backgroundColor = color.CGColor; -} - -@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.h b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.h index d83cd54e..e46a483f 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUINavigationButton.h @@ -37,6 +37,12 @@ typedef NS_ENUM(NSUInteger, QMUINavigationButtonType) { */ @property(nonatomic, assign, readonly) QMUINavigationButtonType type; +/** + * UIBarButtonItem 默认都是跟随 tintColor 的,所以这里声明是否让图片也是用 AlwaysTemplate 模式 + * 默认为 YES + */ +@property(nonatomic, assign) BOOL adjustsImageTintColorAutomatically; + /** * 导航栏按钮的初始化函数,指定的初始化方法 * @param type 按钮类型 @@ -60,13 +66,18 @@ typedef NS_ENUM(NSUInteger, QMUINavigationButtonType) { @interface UIBarButtonItem (QMUINavigationButton) -+ (instancetype)qmui_itemWithButton:(nullable QMUINavigationButton *)button target:(nullable id)target action:(nullable SEL)action; -+ (instancetype)qmui_itemWithImage:(nullable UIImage *)image target:(nullable id)target action:(nullable SEL)action; -+ (instancetype)qmui_itemWithTitle:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)action; -+ (instancetype)qmui_itemWithBoldTitle:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)action; ++ (instancetype)qmui_itemWithButton:(QMUINavigationButton *)button target:(nullable id)target action:(nullable SEL)action; ++ (instancetype)qmui_itemWithImage:(UIImage *)image target:(nullable id)target action:(nullable SEL)action; ++ (instancetype)qmui_itemWithTitle:(NSString *)title target:(nullable id)target action:(nullable SEL)action; ++ (instancetype)qmui_itemWithBoldTitle:(NSString *)title target:(nullable id)target action:(nullable SEL)action; + (instancetype)qmui_backItemWithTitle:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)action; + +/// 返回一个返回按钮,该返回按钮的文字由配置表 NeedsBackBarButtonItemTitle 和 target 的值决定,如果 NeedsBackBarButtonItemTitle 为 NO,则返回按钮不显示文字,若为 YES,则默认文字为“返回”,但如果 target 为 UIViewController 则会自动获取上一个界面的 title 作为当前返回按钮的文字。 + (instancetype)qmui_backItemWithTarget:(nullable id)target action:(nullable SEL)action; + +/// 返回一个以“×”为图片的关闭按钮,“x”的图片使用配置表 NavBarCloseButtonImage 设置 + (instancetype)qmui_closeItemWithTarget:(nullable id)target action:(nullable SEL)action; + + (instancetype)qmui_fixedSpaceItemWithWidth:(CGFloat)width; + (instancetype)qmui_flexibleSpaceItem; @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m index 2b5a24a5..b79bdebb 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUINavigationButton.m @@ -16,10 +16,16 @@ #import "QMUINavigationButton.h" #import "QMUICore.h" #import "UIImage+QMUI.h" +#import "UIColor+QMUI.h" #import "UIViewController+QMUI.h" #import "QMUINavigationController.h" -#import "QMUILog.h" #import "UIControl+QMUI.h" +#import "UIView+QMUI.h" +#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里调整位置 @@ -75,11 +81,13 @@ - (void)renderButtonStyle { self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter; self.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter; self.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; + + // UIBarButtonItem 默认都是跟随 tintColor 的,所以这里让图片也是用 alwaysTemplate 模式 + self.adjustsImageTintColorAutomatically = YES; + if (self.type == QMUINavigationButtonTypeImage) { - if (@available(iOS 11, *)) { - // 让 iOS 11 及以后也能走到 alignmentRectInsets,iOS 10 及以前的系统就算不置为 NO 也可以走到 alignmentRectInsets,从而保证 image 类型的按钮的布局、间距与系统的保持一致 - self.translatesAutoresizingMaskIntoConstraints = NO; - } + // 让 iOS 11 及以后也能走到 alignmentRectInsets,iOS 10 及以前的系统就算不置为 NO 也可以走到 alignmentRectInsets,从而保证 image 类型的按钮的布局、间距与系统的保持一致 + self.translatesAutoresizingMaskIntoConstraints = NO; } // 系统默认对 highlighted 和 disabled 的图片的表现是变身色,但 UIBarButtonItem 是 alpha,为了与 UIBarButtonItem 表现一致,这里禁用了 UIButton 默认的行为,然后通过重写 setImage:forState:,自动将 normal image 处理为对应的 highlighted image 和 disabled image @@ -101,13 +109,11 @@ - (void)renderButtonStyle { } break; case QMUINavigationButtonTypeBack: { - UIImage *backIndicatorImage = [UINavigationBar appearance].backIndicatorImage; + self.qmui_outsideEdge = UIEdgeInsetsMake(-12, -12, -24, -24); + UIImage *backIndicatorImage = UINavigationBar.qmui_appearanceConfigured.backIndicatorImage; if (!backIndicatorImage) { // 配置表没有自定义的图片,则按照系统的返回按钮图片样式创建一张,颜色按照 tintColor 来 - UIColor *tintColor = QMUICMIActivated ? NavBarTintColor : ({ - UIView *view = [[UIView alloc] init]; - view.tintColor; - }); + UIColor *tintColor = QMUICMIActivated ? NavBarTintColor : UIColor.qmui_systemTintColor; backIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavBack size:CGSizeMake(13, 23) lineWidth:3 tintColor:tintColor]; } [self setImage:backIndicatorImage forState:UIControlStateNormal]; @@ -117,10 +123,10 @@ - (void)renderButtonStyle { self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; // @warning 这些数值都是每个iOS版本核对过没问题的,如果修改则要检查要每个版本里与系统UIBarButtonItem的布局是否一致 - UIOffset titleOffsetBaseOnSystem = UIOffsetMake(IOS_VERSION >= 11.0 ? 6 : 7, 0);// 经过这些数值的调整后,自定义返回按钮的位置才能和系统默认返回按钮的位置对准,而配置表里设置的值是在这个调整的基础上再调整 + UIOffset titleOffsetBaseOnSystem = UIOffsetMake(6, 0);// 经过这些数值的调整后,自定义返回按钮的位置才能和系统默认返回按钮的位置对准,而配置表里设置的值是在这个调整的基础上再调整 UIOffset configurationOffset = NavBarBarBackButtonTitlePositionAdjustment; self.titleEdgeInsets = UIEdgeInsetsMake(titleOffsetBaseOnSystem.vertical + configurationOffset.vertical, titleOffsetBaseOnSystem.horizontal + configurationOffset.horizontal, -titleOffsetBaseOnSystem.vertical - configurationOffset.vertical, -titleOffsetBaseOnSystem.horizontal - configurationOffset.horizontal); - self.contentEdgeInsets = UIEdgeInsetsMake(IOS_VERSION < 11.0 ? 1 : 0,// iOS 11 以前,y 值偏移一点 + self.contentEdgeInsets = UIEdgeInsetsMake(0, 0, 0, self.titleEdgeInsets.left); @@ -133,11 +139,11 @@ - (void)renderButtonStyle { } - (void)setImage:(UIImage *)image forState:(UIControlState)state { + if (image && self.adjustsImageTintColorAutomatically) { + image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + } + if (image && [self imageForState:state] != image) { - if (image.renderingMode == UIImageRenderingModeAutomatic) { - // UIBarButtonItem 默认都是跟随 tintColor 的,所以这里让图片也是用 AlwaysTemplate 模式 - image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - } if (state == UIControlStateNormal) { // 将 normal image 处理成对应的 highlighted image 和 disabled image self.defaultHighlightedImage = [[image qmui_imageWithAlpha:NavBarHighlightedAlpha] imageWithRenderingMode:image.renderingMode]; @@ -157,9 +163,40 @@ - (void)setImage:(UIImage *)image forState:(UIControlState)state { } } } + [super setImage:image forState:state]; } +- (void)setAdjustsImageTintColorAutomatically:(BOOL)adjustsImageTintColorAutomatically { + BOOL valueDifference = _adjustsImageTintColorAutomatically != adjustsImageTintColorAutomatically; + _adjustsImageTintColorAutomatically = adjustsImageTintColorAutomatically; + + if (valueDifference) { + [self updateImageRenderingModeIfNeeded]; + } +} + +- (void)updateImageRenderingModeIfNeeded { + if (self.currentImage) { + NSArray *states = @[@(UIControlStateNormal), @(UIControlStateHighlighted), @(UIControlStateSelected), @(UIControlStateSelected|UIControlStateHighlighted), @(UIControlStateDisabled)]; + + for (NSNumber *number in states) { + UIImage *image = [self imageForState:number.unsignedIntegerValue]; + if (!image) { + return; + } + + if (self.adjustsImageTintColorAutomatically) { + // 这里的 setImage: 操作不需要使用 renderingMode 对 image 重新处理,而是放到重写的 setImage:forState 里去做就行了 + [self setImage:image forState:[number unsignedIntegerValue]]; + } else { + // 如果不需要用 template 的模式渲染,并且之前是使用 template 的,则把 renderingMode 改回 original + [self setImage:[image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] forState:[number unsignedIntegerValue]]; + } + } + } +} + // 自定义nav按钮,需要根据这个来修改title的三态颜色。 - (void)tintColorDidChange { [super tintColorDidChange]; @@ -174,16 +211,6 @@ - (UIEdgeInsets)alignmentRectInsets { UIEdgeInsets insets = [super alignmentRectInsets]; if (self.type == QMUINavigationButtonTypeNormal || self.type == QMUINavigationButtonTypeBold) { - // 文字类型的按钮,分别对最左、最右那个按钮调整 inset(这里与 UINavigationItem(QMUINavigationButton) 里的 position 赋值配合使用) - if (@available(iOS 10, *)) { - } else { - if (self.buttonPosition == QMUINavigationButtonPositionLeft) { - insets.left = 8; - } else if (self.buttonPosition == QMUINavigationButtonPositionRight) { - insets.right = 8; - } - } - // 对于奇数大小的字号,不同 iOS 版本的偏移策略不同,统一一下 if (self.titleLabel.font.pointSize / 2.0 > 0) { insets.top = -PixelOne; @@ -200,10 +227,6 @@ - (UIEdgeInsets)alignmentRectInsets { insets.top = 1; } else if (self.type == QMUINavigationButtonTypeBack) { insets.top = PixelOne; - if (@available(iOS 11, *)) { - } else { - insets.left = 8; - } } return insets; @@ -213,27 +236,31 @@ - (UIEdgeInsets)alignmentRectInsets { @implementation UIBarButtonItem (QMUINavigationButton) -+ (instancetype)qmui_itemWithButton:(nullable QMUINavigationButton *)button target:(nullable id)target action:(nullable SEL)action { ++ (instancetype)qmui_itemWithButton:(QMUINavigationButton *)button target:(nullable id)target action:(nullable SEL)action { + if (!button) return nil; [button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside]; - return [[UIBarButtonItem alloc] initWithCustomView:button]; + return [[self alloc] initWithCustomView:button]; } -+ (instancetype)qmui_itemWithImage:(nullable UIImage *)image target:(nullable id)target action:(nullable SEL)action { - return [[UIBarButtonItem alloc] initWithImage:image style:UIBarButtonItemStylePlain target:target action:action]; ++ (instancetype)qmui_itemWithImage:(UIImage *)image target:(nullable id)target action:(nullable SEL)action { + if (!image) return nil; + return [[self alloc] initWithImage:image style:UIBarButtonItemStylePlain target:target action:action]; } -+ (instancetype)qmui_itemWithTitle:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)action { - return [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:target action:action]; ++ (instancetype)qmui_itemWithTitle:(NSString *)title target:(nullable id)target action:(nullable SEL)action { + if (!title) return nil; + return [[self alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:target action:action]; } -+ (instancetype)qmui_itemWithBoldTitle:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)action { - return [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleDone target:target action:action]; ++ (instancetype)qmui_itemWithBoldTitle:(NSString *)title target:(nullable id)target action:(nullable SEL)action { + if (!title) return nil; + return [[self alloc] initWithTitle:title style:UIBarButtonItemStyleDone target:target action:action]; } + (instancetype)qmui_backItemWithTitle:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)action { QMUINavigationButton *button = [[QMUINavigationButton alloc] initWithType:QMUINavigationButtonTypeBack title:title]; [button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside]; - UIBarButtonItem *barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button]; + UIBarButtonItem *barButtonItem = [[self alloc] initWithCustomView:button]; return barButtonItem; } @@ -247,9 +274,9 @@ + (instancetype)qmui_backItemWithTarget:(nullable id)target action:(nullable SEL if (previousViewController.navigationItem.backBarButtonItem) { // 如果前一个界面有主动设置返回按钮的文字,则取这个文字 backTitle = previousViewController.navigationItem.backBarButtonItem.title; - } else if ([viewController respondsToSelector:@selector(backBarButtonItemTitleWithPreviousViewController:)]) { + } else if ([viewController respondsToSelector:@selector(qmui_backBarButtonItemTitleWithPreviousViewController:)]) { // 否则看是否有通过 QMUI 提供的接口来设置返回按钮的文字,有就用它的值 - backTitle = [((UIViewController *)viewController) backBarButtonItemTitleWithPreviousViewController:previousViewController]; + backTitle = [((UIViewController *)viewController) qmui_backBarButtonItemTitleWithPreviousViewController:previousViewController]; } else if (previousViewController.title) { // 否则取上一个界面的标题 backTitle = previousViewController.title; @@ -259,23 +286,23 @@ + (instancetype)qmui_backItemWithTarget:(nullable id)target action:(nullable SEL backTitle = @" "; } - return [UIBarButtonItem qmui_backItemWithTitle:backTitle target:target action:action]; + return [self qmui_backItemWithTitle:backTitle target:target action:action]; } + (instancetype)qmui_closeItemWithTarget:(nullable id)target action:(nullable SEL)action { - UIBarButtonItem *closeItem = [[UIBarButtonItem alloc] initWithImage:NavBarCloseButtonImage style:UIBarButtonItemStylePlain target:target action:action]; + UIBarButtonItem *closeItem = [[self alloc] initWithImage:NavBarCloseButtonImage style:UIBarButtonItemStylePlain target:target action:action]; closeItem.accessibilityLabel = @"关闭"; return closeItem; } + (instancetype)qmui_fixedSpaceItemWithWidth:(CGFloat)width { - UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:NULL]; + UIBarButtonItem *item = [[self alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:NULL]; item.width = width; return item; } + (instancetype)qmui_flexibleSpaceItem { - return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:NULL]; + return [[self alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:NULL]; } @end @@ -292,24 +319,14 @@ @interface UIBarButtonItem (QMUINavigationButton_Private) @property(nonatomic, strong, readonly) QMUINavigationButton *qmui_navigationButton; @end -@interface UINavigationItem (QMUINavigationButton) - -@property(nonatomic, weak, readonly) UINavigationBar *qmui_navigationBar; -@property(nonatomic, copy) NSArray *tempLeftBarButtonItems; -@property(nonatomic, copy) NSArray *tempRightBarButtonItems; -@end - @interface UIViewController (QMUINavigationButton) @end @interface UINavigationBar (QMUINavigationButton) -/// 获取 navigationBar 内部的 contentView -@property(nonatomic, weak, readonly) UIView *qmui_contentView; - /// 判断当前的 UINavigationBar 的返回按钮是不是自定义的 -@property(nonatomic, assign) BOOL qmui_customizingBackBarButtonItem; +@property(nonatomic, readonly) BOOL qmui_customizingBackBarButtonItem; @end @implementation UIBarButtonItem (QMUINavigationButton_Private) @@ -336,9 +353,6 @@ - (QMUINavigationButton *)qmui_navigationButton { @implementation UINavigationItem (QMUINavigationButton) -QMUISynthesizeIdCopyProperty(tempLeftBarButtonItems, setTempLeftBarButtonItems) -QMUISynthesizeIdCopyProperty(tempRightBarButtonItems, setTempRightBarButtonItems) - + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -347,69 +361,23 @@ + (void)load { @selector(setLeftBarButtonItems:animated:), @selector(setRightBarButtonItem:animated:), @selector(setRightBarButtonItems:animated:), - - // 如果被拦截,则 getter 也要返回被缓存的 item,否则会出现这个 bug:https://github.com/Tencent/QMUI_iOS/issues/362 - @selector(leftBarButtonItem), - @selector(leftBarButtonItems), - @selector(rightBarButtonItem), - @selector(rightBarButtonItems) }; 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); } }); } -// 监控是否在 iOS 10 及以下,手势返回的过程中,手势返回背后的那个界面修改了 navigationItem,这可能导致 bug:https://github.com/Tencent/QMUI_iOS/issues/302 -- (BOOL)detectSetItemsWhenPopping { - if (@available(iOS 11, *)) { - } else { - if (self.qmui_navigationBar && [self.qmui_navigationBar.delegate isKindOfClass:[UINavigationController class]]) { - UINavigationController *navController = (UINavigationController *)self.qmui_navigationBar.delegate; - -// QMUILog(@"UINavigationItem (QMUINavigationButton)", @"navigationController is %@, topViewController is %@, viewControllers is %@, willAppearByInteractivePopGestureRecognizer is %@, navigationControllerPopGestureRecognizerChanging is %@", navController, navController.topViewController, navController.viewControllers, StringFromBOOL(navController.topViewController.qmui_willAppearByInteractivePopGestureRecognizer), StringFromBOOL(navController.topViewController.qmui_navigationControllerPopGestureRecognizerChanging)); - - if (navController.topViewController.qmui_willAppearByInteractivePopGestureRecognizer && navController.topViewController.qmui_navigationControllerPopGestureRecognizerChanging) { - // 注意,判断条件里的 qmui_navigationControllerPopGestureRecognizerChanging 关键在于,它是在 viewWillAppear: 执行后才被置为 YES,而 QMUICommonViewController 是在 viewWillAppear: 里调用 setNavigationItems:,所以刚好过滤了这种场景。因为测试过,在 viewWillAppear: 里操作 items 是没问题的,但在那之后的操作就会有问题。 - QMUILog(@"UINavigationItem (QMUINavigationButton)", @"拦截了一次可能产生顶部按钮混乱的操作"); - return YES; - } - } - } - return NO; -} - - (void)qmui_setLeftBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animated { - if ([self detectSetItemsWhenPopping]) { - self.tempLeftBarButtonItems = item ? @[item] : nil; - return; - } - [self qmui_setLeftBarButtonItem:item animated:animated]; // 自动给 position 赋值 item.qmui_navigationButton.buttonPosition = QMUINavigationButtonPositionLeft; - - // iOS 11,调整自定义返回按钮的位置 https://github.com/Tencent/QMUI_iOS/issues/279 - if (@available(iOS 11, *)) { - UINavigationBar *navigationBar = self.qmui_navigationBar; - if (!navigationBar) return; - - // 这个保护对应这个 issue:https://github.com/Tencent/QMUI_iOS/issues/335 - if ([navigationBar.items containsObject:self] && navigationBar.topItem != self) return; - - navigationBar.qmui_customizingBackBarButtonItem = item.qmui_isCustomizedBackBarButtonItem; - } } - (void)qmui_setLeftBarButtonItems:(NSArray *)items animated:(BOOL)animated { - if ([self detectSetItemsWhenPopping]) { - self.tempLeftBarButtonItems = items; - return; - } - [self qmui_setLeftBarButtonItems:items animated:animated]; // 自动给 position 赋值 @@ -420,33 +388,9 @@ - (void)qmui_setLeftBarButtonItems:(NSArray *)items animated: items[i].qmui_navigationButton.buttonPosition = QMUINavigationButtonPositionNone; } } - - // iOS 11,调整自定义返回按钮的位置 https://github.com/Tencent/QMUI_iOS/issues/279 - if (@available(iOS 11, *)) { - - UINavigationBar *navigationBar = self.qmui_navigationBar; - if (!navigationBar) return; - - // 这个保护对应这个 issue:https://github.com/Tencent/QMUI_iOS/issues/335 - if ([navigationBar.items containsObject:self] && navigationBar.topItem != self) return; - - BOOL customizingBackBarButtonItem = NO; - for (UIBarButtonItem *item in items) { - if (item.qmui_isCustomizedBackBarButtonItem) { - customizingBackBarButtonItem = YES; - break; - } - } - navigationBar.qmui_customizingBackBarButtonItem = customizingBackBarButtonItem; - } } - (void)qmui_setRightBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animated { - if ([self detectSetItemsWhenPopping]) { - self.tempRightBarButtonItems = item ? @[item] : nil; - return; - } - [self qmui_setRightBarButtonItem:item animated:animated]; // 自动给 position 赋值 @@ -454,11 +398,6 @@ - (void)qmui_setRightBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animat } - (void)qmui_setRightBarButtonItems:(NSArray *)items animated:(BOOL)animated { - if ([self detectSetItemsWhenPopping]) { - self.tempRightBarButtonItems = items; - return; - } - [self qmui_setRightBarButtonItems:items animated:animated]; // 自动给 position 赋值 @@ -471,42 +410,6 @@ - (void)qmui_setRightBarButtonItems:(NSArray *)items animated } } -- (UIBarButtonItem *)qmui_leftBarButtonItem { - if (self.tempLeftBarButtonItems) { - return self.tempLeftBarButtonItems.firstObject; - } - return [self qmui_leftBarButtonItem]; -} - -- (NSArray *)qmui_leftBarButtonItems { - if (self.tempLeftBarButtonItems) { - return self.tempLeftBarButtonItems; - } - return [self qmui_leftBarButtonItems]; -} - -- (UIBarButtonItem *)qmui_rightBarButtonItem { - if (self.tempRightBarButtonItems) { - return self.tempRightBarButtonItems.firstObject; - } - return [self qmui_rightBarButtonItem]; -} - -- (NSArray *)qmui_rightBarButtonItems { - if (self.tempRightBarButtonItems) { - return self.tempRightBarButtonItems; - } - return [self qmui_rightBarButtonItems]; -} - -- (UINavigationBar *)qmui_navigationBar { - // UINavigationItem 内部有个方法可以获取 navigationBar - if ([self respondsToSelector:@selector(navigationBar)]) { - return [self performSelector:@selector(navigationBar)]; - } - return nil; -} - @end @implementation UIViewController (QMUINavigationButton) @@ -514,15 +417,26 @@ @implementation UIViewController (QMUINavigationButton) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ExtendImplementationOfVoidMethodWithSingleArgument([UIViewController class], @selector(viewDidAppear:), BOOL, ^(UIViewController *selfObject, BOOL firstArgv) { - if (selfObject.navigationItem.tempLeftBarButtonItems) { - selfObject.navigationItem.leftBarButtonItems = selfObject.navigationItem.tempLeftBarButtonItems; - selfObject.navigationItem.tempLeftBarButtonItems = nil; - } - if (selfObject.navigationItem.tempRightBarButtonItems) { - selfObject.navigationItem.rightBarButtonItems = selfObject.navigationItem.tempRightBarButtonItems; - selfObject.navigationItem.tempRightBarButtonItems = nil; - } + + // 当使用自定义返回按钮时,无法使用 VoiceOver 或者 iOS 13.4 新增的 Full Keyboard Access 返回 + OverrideImplementation([UIViewController class], @selector(accessibilityPerformEscape), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^BOOL(UIViewController *selfObject) { + + if (selfObject.navigationItem.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem + && ((QMUINavigationButton *)selfObject.navigationItem.leftBarButtonItem.customView).enabled + && selfObject.navigationController.qmui_rootViewController != selfObject + && selfObject.navigationController.interactivePopGestureRecognizer.enabled + && !UIApplication.sharedApplication.ignoringInteractionEvents) { + [selfObject.navigationController popViewControllerAnimated:YES]; + return YES; + } + + // call super + BOOL (*originSelectorIMP)(id, SEL); + originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider(); + BOOL result = originSelectorIMP(selfObject, originCMD); + return result; + }; }); }); } @@ -535,116 +449,89 @@ + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - OverrideImplementation([UINavigationBar class], @selector(pushNavigationItem:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UINavigationBar *selfObject, UINavigationItem *item, BOOL animated) { + // 强制修改 contentView 的 directionalLayoutMargins.leading,在使用自定义返回按钮时减小 8 + // Xcode11 beta2 修改私有 view 的 directionalLayoutMargins 会 crash,换个方式 + // -[_UINavigationBarContentView directionalLayoutMargins] + NSString *barContentViewString = [NSString qmui_stringByConcat:@"_", @"UINavigationBar", @"ContentView", nil]; + + OverrideImplementation(NSClassFromString(barContentViewString), @selector(directionalLayoutMargins), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^NSDirectionalEdgeInsets(UIView *selfObject) { - // iOS 11,调整自定义返回按钮的位置 https://github.com/Tencent/QMUI_iOS/issues/279 - BOOL shouldSetTagBeforeCallingSuper = NO;// 如果要 push 进的新 item 本身就是自定义返回按钮,那么要先打好标记再调用 super,否则先调用 super 再打标记,实测只有这样才不会导致跳动 - if (@available(iOS 11, *)) { - shouldSetTagBeforeCallingSuper = [selfObject isKindOfClass:originClass] && item.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem; - } + // call super + NSDirectionalEdgeInsets (*originSelectorIMP)(id, SEL); + originSelectorIMP = (NSDirectionalEdgeInsets (*)(id, SEL))originalIMPProvider(); + NSDirectionalEdgeInsets originResult = originSelectorIMP(selfObject, originCMD); - if (shouldSetTagBeforeCallingSuper) { - selfObject.qmui_customizingBackBarButtonItem = item.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem; + // get navbar + UINavigationBar *navBar = nil; + if ([NSStringFromClass([selfObject class]) isEqualToString:barContentViewString] && + [selfObject.superview isKindOfClass:[UINavigationBar class]]) { + navBar = (UINavigationBar *)selfObject.superview; } - // call super - void (*originSelectorIMP)(id, SEL, UINavigationItem *, BOOL); - originSelectorIMP = (void (*)(id, SEL, UINavigationItem *, BOOL))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, item, animated); - - if (!shouldSetTagBeforeCallingSuper) { - selfObject.qmui_customizingBackBarButtonItem = item.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem; + // change insets + if (navBar) { + NSDirectionalEdgeInsets value = originResult; + value.leading -= (navBar.qmui_customizingBackBarButtonItem ? 8 : 0); + return value; } + + return originResult; }; }); - // 对应 UINavigationController setViewControllers:animated: - OverrideImplementation([UINavigationBar class], @selector(setItems:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UINavigationBar *selfObject, NSArray *items, BOOL animated) { + // 系统的 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); + + // result 有值意味着该事件本应属于 bar 的,这时候才干预。 + // 属于 bar 但又分配给容器而不是精准的某个内容 view,此时才考虑扩大点击范围的识别。 + BOOL hitNothing = result == selfObject.qmui_contentView || [NSStringFromClass(result.class) containsString:@"StackView"]; + if (!hitNothing) return result; - if ([selfObject isKindOfClass:originClass]) { - // iOS 11,调整自定义返回按钮的位置 https://github.com/Tencent/QMUI_iOS/issues/279 - if (@available(iOS 11, *)) { - selfObject.qmui_customizingBackBarButtonItem = items.lastObject.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem; + 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; } - - // call super - void (*originSelectorIMP)(id, SEL, NSArray *, BOOL); - originSelectorIMP = (void (*)(id, SEL, NSArray *, BOOL))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, items, animated); + return result; }; }); - - // 强制修改 contentView 的 directionalLayoutMargins.leading,在使用自定义返回按钮时减小 8 - if (@available(iOS 11, *)) { - ExtendImplementationOfVoidMethodWithoutArguments([UINavigationBar class], @selector(layoutSubviews), ^(UINavigationBar *selfObject) { - UIView *contentView = selfObject.qmui_contentView; - if (contentView) { - NSDirectionalEdgeInsets value = contentView.directionalLayoutMargins; - value.leading = value.trailing - (selfObject.qmui_customizingBackBarButtonItem ? 8 : 0); - contentView.directionalLayoutMargins = value; - } - }); - } }); } -- (UIView *)qmui_contentView { - for (UIView *subview in self.subviews) { - if ([NSStringFromClass(subview.class) containsString:@"ContentView"]) { - return subview; - } - } - return nil; -} - -static char kAssociatedObjectKey_customizingBackBarButtonItem; -- (void)setQmui_customizingBackBarButtonItem:(BOOL)qmui_customizingBackBarButtonItem { - objc_setAssociatedObject(self, &kAssociatedObjectKey_customizingBackBarButtonItem, @(qmui_customizingBackBarButtonItem), OBJC_ASSOCIATION_RETAIN_NONATOMIC); - - // 触发 UINavigationBar 的 layoutSubviews,在那里面会去更新 contentView 的 layoutMargins - if (@available(iOS 11, *)) { - [self setNeedsLayout]; - [self layoutIfNeeded]; - } -} - - (BOOL)qmui_customizingBackBarButtonItem { - return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_customizingBackBarButtonItem)) boolValue]; -} - -@end - -@interface UINavigationController (QMUINavigationButton) - -@end - -@implementation UINavigationController (QMUINavigationButton) - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - - // iOS 11,调整自定义返回按钮的位置 https://github.com/Tencent/QMUI_iOS/issues/279 - if (@available(iOS 11, *)) { - - ExtendImplementationOfNonVoidMethodWithTwoArguments([UINavigationController class], @selector(navigationBar:shouldPopItem:), UINavigationBar *, UINavigationItem *, BOOL, ^BOOL(UINavigationController *selfObject, UINavigationBar *navigationBar, UINavigationItem *item, BOOL originReturnValue) { - if (originReturnValue) { - BOOL isPopedByCoding = item != navigationBar.topItem; - if (!isPopedByCoding) { - if (navigationBar.items.count == 2 && !navigationBar.backItem.leftBarButtonItem && item.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem) { - // 如果从自定义返回按钮界面返回到根界面,且根界面左上角没有按钮,则不调整 layout,不然会有跳动。理论上不应该这么改,但暂时没想到优雅的解决方式 - } else { - navigationBar.qmui_customizingBackBarButtonItem = navigationBar.backItem.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem; - } - } - } - return originReturnValue; - }); - } - }); + if (self.topItem.leftBarButtonItem) { + return self.topItem.leftBarButtonItem.qmui_isCustomizedBackBarButtonItem; + } + return NO; } @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.h b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.h index 313f6211..50175867 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIToolbarButton.h @@ -20,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, QMUIToolbarButtonType) { QMUIToolbarButtonTypeNormal, // 普通工具栏按钮 QMUIToolbarButtonTypeRed, // 工具栏红色按钮,用于删除等警告性操作 - QMUIToolbarButtonTypeImage, // 图标类型的按钮 + QMUIToolbarButtonTypeImage, // 图标类型的按钮 }; /** diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.m b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.m index a20ca606..6a7b5652 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIToolbarButton.m diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.h index ecd1b3f5..cd123fd7 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUICellHeightCache.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.m index f1dee38f..75875590 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUICellHeightCache.m @@ -17,6 +17,7 @@ #import "QMUITableViewProtocols.h" #import "QMUICore.h" #import "UIScrollView+QMUI.h" +#import "UITableView+QMUI.h" #import "UIView+QMUI.h" #import "NSNumber+QMUI.h" @@ -153,7 +154,7 @@ - (QMUICellHeightCache *)qmui_keyedHeightCache { if (!self.qmuiTableCache_allKeyedHeightCaches) { self.qmuiTableCache_allKeyedHeightCaches = [[NSMutableDictionary alloc] init]; } - CGFloat contentWidth = CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.qmui_contentInset); + CGFloat contentWidth = self.qmui_validContentWidth; QMUICellHeightCache *cache = self.qmuiTableCache_allKeyedHeightCaches[@(contentWidth)]; if (!cache) { cache = [[QMUICellHeightCache alloc] init]; @@ -162,9 +163,9 @@ - (QMUICellHeightCache *)qmui_keyedHeightCache { return cache; } -- (void)qmui_invalidateHeightForKey:(id)key { +- (void)qmui_invalidateHeightForKey:(id)aKey { [self.qmuiTableCache_allKeyedHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, QMUICellHeightCache * _Nonnull obj, BOOL * _Nonnull stop) { - [obj invalidateHeightForKey:key]; + [obj invalidateHeightForKey:aKey]; }]; } @@ -179,7 +180,7 @@ - (QMUICellHeightIndexPathCache *)qmui_indexPathHeightCache { if (!self.qmuiTableCache_allIndexPathHeightCaches) { self.qmuiTableCache_allIndexPathHeightCaches = [[NSMutableDictionary alloc] init]; } - CGFloat contentWidth = CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.qmui_contentInset); + CGFloat contentWidth = self.qmui_validContentWidth; QMUICellHeightIndexPathCache *cache = self.qmuiTableCache_allIndexPathHeightCaches[@(contentWidth)]; if (!cache) { cache = [[QMUICellHeightIndexPathCache alloc] init]; @@ -221,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); } }); } @@ -366,7 +367,7 @@ - (void)qmuiTableCache_moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndex @implementation UITableView (QMUILayoutCell) - (__kindof UITableViewCell *)templateCellForReuseIdentifier:(NSString *)identifier { - NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier); + QMUIAssert(identifier.length > 0, @"QMUICellHeightCache", @"%s 需要一个合法的 identifier", __func__); NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd); if (!templateCellsByIdentifiers) { templateCellsByIdentifiers = @{}.mutableCopy; @@ -382,8 +383,8 @@ - (__kindof UITableViewCell *)templateCellForReuseIdentifier:(NSString *)identif // 没有的话,则需要通过register来注册一个cell,否则会crash if (!templateCell) { templateCell = [self dequeueReusableCellWithIdentifier:identifier]; - NSAssert(templateCell != nil, @"Cell must be registered to table view for identifier - %@", identifier); } + QMUIAssert(templateCell != nil, @"QMUICellHeightCache", @"通过 %s %@ 无法得到一个 cell 对象", __func__, identifier); templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO; templateCellsByIdentifiers[identifier] = templateCell; } @@ -391,13 +392,13 @@ - (__kindof UITableViewCell *)templateCellForReuseIdentifier:(NSString *)identif } - (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(__kindof UITableViewCell *))configuration { - if (!identifier || CGRectIsEmpty(self.bounds)) { + CGFloat contentWidth = self.qmui_validContentWidth; + if (!identifier || contentWidth <= 0) { return 0; } UITableViewCell *cell = [self templateCellForReuseIdentifier:identifier]; [cell prepareForReuse]; if (configuration) configuration(cell); - CGFloat contentWidth = CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.qmui_contentInset); CGSize fitSize = CGSizeZero; if (cell && contentWidth > 0) { fitSize = [cell sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)]; @@ -407,7 +408,7 @@ - (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier configuration // 通过indexPath缓存高度 - (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(__kindof UITableViewCell *))configuration { - if (!identifier || !indexPath || CGRectIsEmpty(self.bounds)) { + if (!identifier || !indexPath || self.qmui_validContentWidth <= 0) { return 0; } if ([self.qmui_indexPathHeightCache existsHeightAtIndexPath:indexPath]) { @@ -420,7 +421,7 @@ - (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexP // 通过key缓存高度 - (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id)key configuration:(void (^)(__kindof UITableViewCell *))configuration { - if (!identifier || !key || CGRectIsEmpty(self.bounds)) { + if (!identifier || !key || self.qmui_validContentWidth <= 0) { return 0; } if ([self.qmui_keyedHeightCache existsHeightForKey:key]) { @@ -457,7 +458,7 @@ - (QMUICellHeightCache *)qmui_keyedHeightCache { if (!self.qmuiCollectionCache_allKeyedHeightCaches) { self.qmuiCollectionCache_allKeyedHeightCaches = [[NSMutableDictionary alloc] init]; } - CGSize collectionViewSize = CGSizeMake(CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.qmui_contentInset), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.qmui_contentInset)); + CGSize collectionViewSize = CGSizeMake(CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.safeAreaInsets), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.safeAreaInsets)); QMUICellHeightCache *cache = self.qmuiCollectionCache_allKeyedHeightCaches[[NSValue valueWithCGSize:collectionViewSize]]; if (!cache) { cache = [[QMUICellHeightCache alloc] init]; @@ -466,9 +467,9 @@ - (QMUICellHeightCache *)qmui_keyedHeightCache { return cache; } -- (void)qmui_invalidateHeightForKey:(id)key { +- (void)qmui_invalidateHeightForKey:(id)aKey { [self.qmuiCollectionCache_allKeyedHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSValue * _Nonnull key, QMUICellHeightCache * _Nonnull obj, BOOL * _Nonnull stop) { - [obj invalidateHeightForKey:key]; + [obj invalidateHeightForKey:aKey]; }]; } @@ -483,7 +484,7 @@ - (QMUICellHeightIndexPathCache *)qmui_indexPathHeightCache { if (!self.qmuiCollectionCache_allIndexPathHeightCaches) { self.qmuiCollectionCache_allIndexPathHeightCaches = [[NSMutableDictionary alloc] init]; } - CGSize collectionViewSize = CGSizeMake(CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.qmui_contentInset), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.qmui_contentInset)); + CGSize collectionViewSize = CGSizeMake(CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.safeAreaInsets), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.safeAreaInsets)); QMUICellHeightIndexPathCache *cache = self.qmuiCollectionCache_allIndexPathHeightCaches[[NSValue valueWithCGSize:collectionViewSize]]; if (!cache) { cache = [[QMUICellHeightIndexPathCache alloc] init]; @@ -525,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); } }); } @@ -670,9 +671,8 @@ - (void)qmuiCollectionCache_moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath t @implementation UICollectionView (QMUILayoutCell) - (__kindof UICollectionViewCell *)templateCellForReuseIdentifier:(NSString *)identifier cellClass:(Class)cellClass { - NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier); - NSAssert([self.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]], @"only flow layout accept"); - NSAssert([cellClass isSubclassOfClass:[UICollectionViewCell class]], @"must be uicollection view cell"); + QMUIAssert(identifier.length > 0, @"QMUICellHeightCache", @"%s 需要一个合法的 identifier", __func__); + QMUIAssert([self.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]], @"QMUICellHeightCache", @"只支持 %@", NSStringFromClass(UICollectionViewFlowLayout.class)); NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd); if (!templateCellsByIdentifiers) { templateCellsByIdentifiers = @{}.mutableCopy; @@ -683,7 +683,7 @@ - (__kindof UICollectionViewCell *)templateCellForReuseIdentifier:(NSString *)id // CollecionView 跟 TableView 不太一样,无法通过 dequeueReusableCellWithReuseIdentifier:forIndexPath: 来拿到cell(如果这样做,首先indexPath不知道传什么值,其次是这样做会已知crash,说数组越界),所以只能通过传一个class来通过init方法初始化一个cell,但是也有缓存来复用cell。 // templateCell = [self dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; templateCell = [[cellClass alloc] initWithFrame:CGRectZero]; - NSAssert(templateCell != nil, @"Cell must be registered to collection view for identifier - %@", identifier); + QMUIAssert(templateCell != nil, @"QMUICellHeightCache", @"通过 %s %@ 无法得到一个 cell 对象", __func__, identifier); } templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO; templateCellsByIdentifiers[identifier] = templateCell; @@ -691,7 +691,7 @@ - (__kindof UICollectionViewCell *)templateCellForReuseIdentifier:(NSString *)id } - (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth configuration:(void (^)(__kindof UICollectionViewCell *cell))configuration { - if (!identifier || CGRectIsEmpty(self.bounds)) { + if (!identifier || CGRectGetWidth(self.bounds) <= 0) { return 0; } UICollectionViewCell *cell = [self templateCellForReuseIdentifier:identifier cellClass:cellClass]; @@ -706,7 +706,7 @@ - (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Cl // 通过indexPath缓存高度 - (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(__kindof UICollectionViewCell *cell))configuration { - if (!identifier || !indexPath || CGRectIsEmpty(self.bounds)) { + if (!identifier || !indexPath || CGRectGetWidth(self.bounds) <= 0) { return 0; } if ([self.qmui_indexPathHeightCache existsHeightAtIndexPath:indexPath]) { @@ -719,7 +719,7 @@ - (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Cl // 通过key缓存高度 - (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth cacheByKey:(id)key configuration:(void (^)(__kindof UICollectionViewCell *cell))configuration { - if (!identifier || !key || CGRectIsEmpty(self.bounds)) { + if (!identifier || !key || CGRectGetWidth(self.bounds) <= 0) { return 0; } if ([self.qmui_keyedHeightCache existsHeightForKey:key]) { diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.h index 7fc459b4..9c4be7c9 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUICellHeightKeyCache.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.m index 8379b931..fa2e0838 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUICellHeightKeyCache.m diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.h index 866cc4fd..c7ff7dcd 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITableView+QMUICellHeightKeyCache.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.m index 2b27470b..a6198636 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITableView+QMUICellHeightKeyCache.m @@ -18,6 +18,7 @@ #import "QMUICellHeightKeyCache.h" #import "UIView+QMUI.h" #import "UIScrollView+QMUI.h" +#import "UITableView+QMUI.h" #import "QMUITableViewProtocols.h" #import "QMUIMultipleDelegates.h" @@ -34,10 +35,7 @@ + (void)load { OverrideImplementation([UITableView class], @selector(setDelegate:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UITableView *selfObject, id firstArgv) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - [selfObject replaceMethodForDelegateIfNeeded:firstArgv]; - } + [selfObject replaceMethodForDelegateIfNeeded:firstArgv]; // call super void (*originSelectorIMP)(id, SEL, id); @@ -54,8 +52,8 @@ - (void)setQmui_cacheCellHeightByKeyAutomatically:(BOOL)qmui_cacheCellHeightByKe if (qmui_cacheCellHeightByKeyAutomatically) { - NSAssert(!self.delegate || [self.delegate respondsToSelector:@selector(qmui_tableView:cacheKeyForRowAtIndexPath:)], @"%@ 需要实现 %@ 方法才能自动缓存 cell 高度", self.delegate, NSStringFromSelector(@selector(qmui_tableView:cacheKeyForRowAtIndexPath:))); - NSAssert(self.estimatedRowHeight != 0 || [self.delegate respondsToSelector:@selector(tableView:estimatedHeightForRowAtIndexPath:)], @"必须为 estimatedRowHeight 赋一个不为0的值,或者实现 tableView:estimatedHeightForRowAtIndexPath: 方法,否则无法开启 self-sizing cells 功能"); + QMUIAssert(!self.delegate || [self.delegate respondsToSelector:@selector(qmui_tableView:cacheKeyForRowAtIndexPath:)], @"QMUICellHeightKeyCache", @"%@ 需要实现 %@ 方法才能自动缓存 cell 高度", self.delegate, NSStringFromSelector(@selector(qmui_tableView:cacheKeyForRowAtIndexPath:))); + QMUIAssert(self.estimatedRowHeight != 0 || [self.delegate respondsToSelector:@selector(tableView:estimatedHeightForRowAtIndexPath:)], @"QMUICellHeightKeyCache", @"必须为 estimatedRowHeight 赋一个不为0的值,或者实现 tableView:estimatedHeightForRowAtIndexPath: 方法,否则无法开启 self-sizing cells 功能"); [self replaceMethodForDelegateIfNeeded:(id)self.delegate]; @@ -81,7 +79,7 @@ - (void)setQmui_allKeyCaches:(NSMutableDictionary cachedKey = [((id)tableView.delegate) qmui_tableView:tableView cacheKeyForRowAtIndexPath:indexPath]; @@ -130,22 +122,15 @@ - (CGFloat)qmui_tableView:(UITableView *)tableView estimatedHeightForRowAtIndexP return UITableViewAutomaticDimension;// 表示 QMUICellHeightKeyCache 无法决定一个合适的高度,交给业务,或者交给系统默认值决定。 } -static NSMutableSet *qmui_methodsReplacedClasses; - (void)replaceMethodForDelegateIfNeeded:(id)delegate { if (self.qmui_cacheCellHeightByKeyAutomatically && delegate) { - if (!qmui_methodsReplacedClasses) { - qmui_methodsReplacedClasses = [NSMutableSet set]; - } void (^addSelectorBlock)(id) = ^void(id aDelegate) { - if ([qmui_methodsReplacedClasses containsObject:NSStringFromClass(aDelegate.class)]) { - return; - } - [qmui_methodsReplacedClasses addObject:NSStringFromClass(aDelegate.class)]; - - [self handleWillDisplayCellMethodForDelegate:aDelegate]; - [self handleHeightForRowMethodForDelegate:aDelegate]; - [self handleEstimatedHeightForRowMethodForDelegate:aDelegate]; + [QMUIHelper executeBlock:^{ + [self handleWillDisplayCellMethodForDelegate:aDelegate]; + [self handleHeightForRowMethodForDelegate:aDelegate]; + [self handleEstimatedHeightForRowMethodForDelegate:aDelegate]; + } oncePerIdentifier:[NSString stringWithFormat:@"QMUICellHeightKeyCache %@", NSStringFromClass(aDelegate.class)]]; }; if ([delegate isKindOfClass:[QMUIMultipleDelegates class]]) { @@ -180,9 +165,6 @@ - (void)handleWillDisplayCellMethodForDelegate:(id)delega originSelectorIMP = (void (*)(id, SEL, UITableView *, UITableViewCell *, NSIndexPath *))originalIMPProvider(); originSelectorIMP(delegateSelf, originCMD, tableView, cell, indexPath); - // avoid superclass - if (![delegateSelf isKindOfClass:originClass]) return; - // call QMUI willDisplayCellFunction(delegateSelf, willDisplayCellSelector, tableView, cell, indexPath); }; @@ -209,9 +191,6 @@ - (void)handleHeightForRowMethodForDelegate:(id)delegate originSelectorIMP = (CGFloat (*)(id, SEL, UITableView *, NSIndexPath *))originalIMPProvider(); CGFloat result = originSelectorIMP(delegateSelf, originCMD, tableView, indexPath); - // avoid superclass - if (![delegateSelf isKindOfClass:originClass]) return result; - if (result >= 0) { return result; } @@ -237,12 +216,9 @@ - (void)handleEstimatedHeightForRowMethodForDelegate:(id) OverrideImplementation([delegate class], heightForRowSelector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^CGFloat(id delegateSelf, UITableView *tableView, NSIndexPath *indexPath) { - CGFloat result = 0; - if ([delegateSelf isKindOfClass:originClass]) { - result = heightForRowFunction(delegateSelf, heightForRowSelector, tableView, indexPath); - if (result != UITableViewAutomaticDimension) { - return result; - } + CGFloat result = heightForRowFunction(delegateSelf, heightForRowSelector, tableView, indexPath); + if (result != UITableViewAutomaticDimension) { + return result; } // call super diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.h index a865fa60..c97a7c87 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUICellSizeKeyCache.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.m index 79adafea..8f083b4a 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUICellSizeKeyCache.m diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.h index a280777f..8ba4b741 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UICollectionView+QMUICellSizeKeyCache.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.m index 3fc66847..54b71dae 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UICollectionView+QMUICellSizeKeyCache.m @@ -80,10 +80,7 @@ + (void)load { OverrideImplementation([UICollectionView class], @selector(setDelegate:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UICollectionView *selfObject, id firstArgv) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - [selfObject replaceMethodForDelegateIfNeeded:firstArgv]; - } + [selfObject replaceMethodForDelegateIfNeeded:firstArgv]; // call super void (*originSelectorIMP)(id, SEL, id); @@ -99,7 +96,7 @@ - (void)setQmui_cacheCellSizeByKeyAutomatically:(BOOL)qmui_cacheCellSizeByKeyAut objc_setAssociatedObject(self, &kAssociatedObjectKey_qmuiCacheCellSizeByKeyAutomatically, @(qmui_cacheCellSizeByKeyAutomatically), OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (qmui_cacheCellSizeByKeyAutomatically) { - NSAssert([self.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]], @"QMUICellSizeKeyCache 只支持 UICollectionViewFlowLayout"); + QMUIAssert([self.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]], @"QMUICellSizeKeyCache", @"只支持 %@", NSStringFromClass(UICollectionViewFlowLayout.class)); [self replaceMethodForDelegateIfNeeded:self.delegate]; @@ -143,10 +140,10 @@ - (QMUICellSizeKeyCache *)qmui_currentCellSizeKeyCache { - (CGFloat)widthForCacheKey { UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.collectionViewLayout; if (layout.scrollDirection == UICollectionViewScrollDirectionHorizontal) { - CGFloat height = CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.qmui_contentInset) - UIEdgeInsetsGetVerticalValue(layout.sectionInset); + CGFloat height = CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.adjustedContentInset) - UIEdgeInsetsGetVerticalValue(layout.sectionInset); return height; } - CGFloat width = CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.qmui_contentInset) - UIEdgeInsetsGetHorizontalValue(((UICollectionViewFlowLayout *)self.collectionViewLayout).sectionInset); + CGFloat width = CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.adjustedContentInset) - UIEdgeInsetsGetHorizontalValue(((UICollectionViewFlowLayout *)self.collectionViewLayout).sectionInset); return width; } @@ -154,7 +151,8 @@ - (void)qmui_collectionView:(UICollectionView *)collectionView willDisplayCell:( [collectionView qmui_collectionView:collectionView willDisplayCell:cell forItemAtIndexPath:indexPath]; if (collectionView.qmui_cacheCellSizeByKeyAutomatically) { if (![collectionView.delegate respondsToSelector:@selector(qmui_collectionView:cacheKeyForItemAtIndexPath:)]) { - NSAssert(NO, @"%@ 需要实现 %@ 方法才能自动缓存 cell 高度", collectionView.delegate, NSStringFromSelector(@selector(qmui_collectionView:cacheKeyForItemAtIndexPath:))); + QMUIAssert(NO, @"QMUICellSizeKeyCache", @"%@ 需要实现 %@ 方法才能自动缓存 cell 高度", collectionView.delegate, NSStringFromSelector(@selector(qmui_collectionView:cacheKeyForItemAtIndexPath:))); + return; } id cachedKey = [((id)self) qmui_collectionView:collectionView cacheKeyForItemAtIndexPath:indexPath]; [collectionView.qmui_currentCellSizeKeyCache cacheSize:cell.frame.size forKey:cachedKey]; @@ -180,30 +178,24 @@ - (void)qmui_collectionView:(UICollectionView *)collectionView willDisplayCell:( // return collectionViewLayout.estimatedItemSize; //} -static NSMutableSet *qmui_methodsReplacedClasses; - (void)replaceMethodForDelegateIfNeeded:(id)delegate { - if (self.qmui_cacheCellSizeByKeyAutomatically && delegate) { - if (!qmui_methodsReplacedClasses) { - qmui_methodsReplacedClasses = [NSMutableSet set]; - } - void (^addSelectorBlock)(id) = ^void(id aDelegate) { - if ([qmui_methodsReplacedClasses containsObject:NSStringFromClass(aDelegate.class)]) { - return; - } - [qmui_methodsReplacedClasses addObject:NSStringFromClass(aDelegate.class)]; - }; - - if ([delegate isKindOfClass:[QMUIMultipleDelegates class]]) { - NSPointerArray *delegates = [((QMUIMultipleDelegates *)delegate).delegates copy]; - for (id d in delegates) { - if ([d conformsToProtocol:@protocol(UICollectionViewDelegate)]) { - addSelectorBlock((id)d); - } - } - } else { - addSelectorBlock((id)delegate); - } - } +// if (self.qmui_cacheCellSizeByKeyAutomatically && delegate) { +// void (^addSelectorBlock)(id) = ^void(id aDelegate) { +// [QMUIHelper executeBlock:^{ +// } oncePerIdentifier:[NSString stringWithFormat:@"QMUICellHeightKeyCache collectionView %@", NSStringFromClass(aDelegate.class)]]; +// }; +// +// if ([delegate isKindOfClass:[QMUIMultipleDelegates class]]) { +// NSPointerArray *delegates = [((QMUIMultipleDelegates *)delegate).delegates copy]; +// for (id d in delegates) { +// if ([d conformsToProtocol:@protocol(UICollectionViewDelegate)]) { +// addSelectorBlock((id)d); +// } +// } +// } else { +// addSelectorBlock((id)delegate); +// } +// } } @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICheckbox.h b/QMUI/QMUIKit/QMUIComponents/QMUICheckbox.h new file mode 100644 index 00000000..df36f94b --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUICheckbox.h @@ -0,0 +1,40 @@ +// +// QMUICheckbox.h +// QMUIKit +// +// Created by molice on 2024/8/1. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import +#import "QMUIButton.h" + +NS_ASSUME_NONNULL_BEGIN + +/// 圆形勾选控件,selected = YES 表示勾选,indeterminate = YES 表示半选,enabled = NO 表示禁用。 +/// 由于父类是 QMUIButton,所以可以通过 setTitle:forState: 轻松实现左边 checkbox 右边说明文本的效果。 +/// 尺寸可以通过 checkboxSize 修改,颜色可通过 tintColor 修改。 +/// 点击勾选的交互需要由业务自己实现。 +@interface QMUICheckbox : QMUIButton + +/// 置为半选状态。可以理解为一个 Checkbox 的 indeterminate 和 checked(selected) 是平级的、互斥的,当该属性被设置为 YES 时,会将 selected 置为 NO,当 selected 被置为 YES 时,会将该属性置为 NO。 +@property(nonatomic, assign) BOOL indeterminate; + +/// 指定 checkbox 图片的尺寸(如果存在 title,不影响 title 的尺寸) +/// 默认为(16, 16) +@property(nonatomic, assign) CGSize checkboxSize; + +/// 未勾选的状态,置为 nil 则使用组件默认图 +@property(nonatomic, strong) UIImage *normalImage; + +/// 勾选的状态,置为 nil 则使用组件默认图 +@property(nonatomic, strong) UIImage *selectedImage; + +/// 半勾选的状态,置为 nil 则使用组件默认图 +@property(nonatomic, strong) UIImage *indeterminateImage; + +/// 未勾选且禁用的状态(如果是已勾选的禁用,会直接沿用该状态的图片,只有未勾选的禁用可以有单独的图),置为 nil 则使用组件默认图 +@property(nonatomic, strong) UIImage *disabledImage; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICheckbox.m b/QMUI/QMUIKit/QMUIComponents/QMUICheckbox.m new file mode 100644 index 00000000..77d6046e --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUICheckbox.m @@ -0,0 +1,104 @@ +// +// QMUICheckbox.m +// QMUIKit +// +// Created by molice on 2024/8/1. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import "QMUICheckbox.h" +#import "QMUICore.h" +#import "CALayer+QMUI.h" +#import "UIView+QMUI.h" + +@interface QMUICheckbox () +@property(nonatomic, strong) UIImageView *indeterminateImageView; +@property(nonatomic, strong) CALayer *imageViewMaks; +@end + +@implementation QMUICheckbox + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.normalImage = self.normalImage; + self.selectedImage = self.selectedImage; + self.indeterminateImage = self.indeterminateImage; + self.disabledImage = self.disabledImage; + + _checkboxSize = self.currentImage.size; + self.imageView.contentMode = UIViewContentModeScaleToFill; + self.qmui_outsideEdge = UIEdgeInsetsMake(-8, -8, -8, -8); + } + return self; +} + +- (void)setNormalImage:(UIImage *)normalImage { + _normalImage = normalImage ?: [[QMUIHelper imageWithName:@"QMUI_checkbox16"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [self setImage:_normalImage forState:UIControlStateNormal]; +} + +- (void)setSelectedImage:(UIImage *)selectedImage { + _selectedImage = selectedImage ?: [[QMUIHelper imageWithName:@"QMUI_checkbox16_checked"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [self setImage:_selectedImage forState:UIControlStateSelected]; + [self setImage:_selectedImage forState:UIControlStateSelected|UIControlStateHighlighted]; + [self setImage:_selectedImage forState:UIControlStateSelected|UIControlStateDisabled]; +} + +- (void)setDisabledImage:(UIImage *)disabledImage { + _disabledImage = disabledImage ?: [[QMUIHelper imageWithName:@"QMUI_checkbox16_disabled"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [self setImage:_disabledImage forState:UIControlStateDisabled]; +} + +- (void)setIndeterminateImage:(UIImage *)indeterminateImage { + _indeterminateImage = indeterminateImage ?: [[QMUIHelper imageWithName:@"QMUI_checkbox16_indeterminate"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; +} + +- (void)setIndeterminate:(BOOL)indeterminate { + BOOL valueChanged = _indeterminate != indeterminate; + if (!valueChanged) return; + + _indeterminate = indeterminate; + if (indeterminate) { + if (self.selected) { + self.selected = NO; + } + if (!self.indeterminateImageView) { + self.indeterminateImageView = [[UIImageView alloc] init]; + self.indeterminateImageView.contentMode = UIViewContentModeScaleToFill; + [self addSubview:self.indeterminateImageView]; + } + if (!self.imageViewMaks) { + self.imageViewMaks = CALayer.layer; + [self.imageViewMaks qmui_removeDefaultAnimations]; + } + self.indeterminateImageView.image = self.indeterminateImage; + self.indeterminateImageView.hidden = NO; + self.imageView.layer.mask = self.imageViewMaks;// 保持 imageView 布局不变的情况下让 imageView 不可见 + [self setNeedsLayout]; + } else { + self.indeterminateImageView.hidden = YES; + self.imageView.layer.mask = nil; + } +} + +- (void)setSelected:(BOOL)selected { + [super setSelected:selected]; + if (selected && self.indeterminate) { + self.indeterminate = NO; + } +} + +- (void)layoutSubviews { + [super layoutSubviews]; + self.indeterminateImageView.frame = self.imageView.frame; +} + +- (void)setCheckboxSize:(CGSize)checkboxSize { + if (CGSizeIsEmpty(checkboxSize)) return; + _checkboxSize = checkboxSize; + self.imageView.qmui_fixedSize = checkboxSize; + self.indeterminateImageView.qmui_fixedSize = checkboxSize; + [self setNeedsLayout]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.h b/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.h index c272909c..b532188d 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUICollectionViewPagingLayout.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.m b/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.m index b0b13587..24f96227 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUICollectionViewPagingLayout.m @@ -155,9 +155,9 @@ - (void)prepareLayout { if (self.debugLayer) { if (self.scrollDirection == UICollectionViewScrollDirectionVertical) { - self.debugLayer.frame = CGRectFlatMake(0, self.collectionView.qmui_contentInset.top + self.sectionInset.top + _finalItemSize.height / 2, CGRectGetWidth(self.collectionView.bounds), PixelOne); + self.debugLayer.frame = CGRectFlatMake(0, self.collectionView.adjustedContentInset.top + self.sectionInset.top + _finalItemSize.height / 2, CGRectGetWidth(self.collectionView.bounds), PixelOne); } else { - self.debugLayer.frame = CGRectFlatMake(self.collectionView.qmui_contentInset.left + self.sectionInset.left + _finalItemSize.width / 2, 0, PixelOne, CGRectGetHeight(self.collectionView.bounds)); + self.debugLayer.frame = CGRectFlatMake(self.collectionView.adjustedContentInset.left + self.sectionInset.left + _finalItemSize.width / 2, 0, PixelOne, CGRectGetHeight(self.collectionView.bounds)); } } } @@ -232,7 +232,7 @@ - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentO CGSize contentSize = self.collectionViewContentSize; CGSize frameSize = self.collectionView.bounds.size; - UIEdgeInsets contentInset = self.collectionView.qmui_contentInset; + UIEdgeInsets contentInset = self.collectionView.adjustedContentInset; BOOL scrollingToRight = proposedContentOffset.x < self.collectionView.contentOffset.x;// 代表 collectionView 期望的实际滚动方向是向右,但不代表手指拖拽的方向是向右,因为有可能此时已经在左边的尽头,继续往右拖拽,松手的瞬间由于回弹,这里会判断为是想向左滚动,但其实你的手指是向右拖拽 BOOL scrollingToBottom = proposedContentOffset.y < self.collectionView.contentOffset.y; @@ -252,11 +252,7 @@ - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentO // 最顶/最底 if (proposedContentOffset.y < -contentInset.top || proposedContentOffset.y >= contentSize.height + contentInset.bottom - frameSize.height) { - if (IOS_VERSION_NUMBER < 100000) { - // iOS 10 及以上的版本,直接返回当前的 contentOffset,系统会自动帮你调整到边界状态,而 iOS 9 及以下的版本需要自己计算 - // https://github.com/Tencent/QMUI_iOS/issues/499 - proposedContentOffset.y = MIN(MAX(proposedContentOffset.y, -contentInset.top), contentSize.height + contentInset.bottom - frameSize.height); - } + // iOS 10 及以上的版本,直接返回当前的 contentOffset,系统会自动帮你调整到边界状态,而 iOS 9 及以下的版本需要自己计算 return proposedContentOffset; } @@ -269,8 +265,8 @@ - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentO } else { CGFloat remainder = progress - currentIndex; CGFloat offset = remainder * itemSpacing; - BOOL shouldNext = (forcePaging && !scrollingToBottom) ? YES : (offset / _finalItemSize.height >= self.pagingThreshold); - BOOL shouldPrev = (forcePaging && scrollingToBottom) ? YES : (offset / _finalItemSize.height <= 1 - self.pagingThreshold); + BOOL shouldNext = (forcePaging || (offset / _finalItemSize.height >= self.pagingThreshold)) && !scrollingToBottom && velocity.y > 0; + BOOL shouldPrev = (forcePaging || (offset / _finalItemSize.height <= 1 - self.pagingThreshold)) && scrollingToBottom && velocity.y < 0; targetIndex = currentIndex + (shouldNext ? 1 : (shouldPrev ? -1 : 0)); } proposedContentOffset.y = -contentInset.top + targetIndex * itemSpacing; @@ -287,11 +283,7 @@ - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentO // 最左/最右 if (proposedContentOffset.x < -contentInset.left || proposedContentOffset.x >= contentSize.width + contentInset.right - frameSize.width) { - if (IOS_VERSION_NUMBER < 100000) { - // iOS 10 及以上的版本,直接返回当前的 contentOffset,系统会自动帮你调整到边界状态,而 iOS 9 及以下的版本需要自己计算 - // https://github.com/Tencent/QMUI_iOS/issues/499 - proposedContentOffset.x = MIN(MAX(proposedContentOffset.x, -contentInset.left), contentSize.width + contentInset.right - frameSize.width); - } + // iOS 10 及以上的版本,直接返回当前的 contentOffset,系统会自动帮你调整到边界状态,而 iOS 9 及以下的版本需要自己计算 return proposedContentOffset; } @@ -304,8 +296,9 @@ - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentO } else { CGFloat remainder = progress - currentIndex; CGFloat offset = remainder * itemSpacing; - BOOL shouldNext = (forcePaging && !scrollingToRight) ? YES : (offset / _finalItemSize.width >= self.pagingThreshold); - BOOL shouldPrev = (forcePaging && scrollingToRight) ? YES : (offset / _finalItemSize.width <= 1 - self.pagingThreshold); + // collectionView 关闭了 bounces 后,如果在第一页向左边快速滑动一段距离,并不会触发上一个「最左/最右」的判断(因为 proposedContentOffset 不够),此时的 velocity 为负数,所以要加上 velocity.x > 0 的判断,否则这种情况会命中 forcePaging && !scrollingToRight 这两个条件,当做下一页处理。 + BOOL shouldNext = (forcePaging || (offset / _finalItemSize.width >= self.pagingThreshold)) && !scrollingToRight && velocity.x > 0; + BOOL shouldPrev = (forcePaging || (offset / _finalItemSize.width <= 1 - self.pagingThreshold)) && scrollingToRight && velocity.x < 0; targetIndex = currentIndex + (shouldNext ? 1 : (shouldPrev ? -1 : 0)); } proposedContentOffset.x = -contentInset.left + targetIndex * itemSpacing; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.h b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.h index 098e0c15..27211b87 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.h @@ -1,17 +1,16 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIConsole.h // QMUIKit // // Created by MoLice on 2019/J/11. -// Copyright © 2019 QMUI Team. All rights reserved. // #import diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m index aa134404..5e7ed03f 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m @@ -1,17 +1,16 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIConsole.m // QMUIKit // // Created by MoLice on 2019/J/11. -// Copyright © 2019 QMUI Team. All rights reserved. // #import "QMUIConsole.h" @@ -22,9 +21,37 @@ #import "UIColor+QMUI.h" #import "QMUITextView.h" +/// 定义一个 class 只是为了在 Lookin 里表达这是一个 console window 而已,不需要实现什么东西 +@interface QMUIConsoleWindow : UIWindow +@end + +@implementation QMUIConsoleWindow + +- (instancetype)init { + if (self = [super init]) { + self.backgroundColor = nil; + if (QMUICMIActivated) { + self.windowLevel = UIWindowLevelQMUIConsole; + } else { + self.windowLevel = 1; + } + self.qmui_capturesStatusBarAppearance = NO; + } + return self; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + // 当显示 QMUIConsole 时,点击空白区域,consoleViewController hitTest 会 return nil,从而将事件传递给 window,再由 window hitTest return nil 来把事件传递给 UIApplication.delegate.window。但在 iPad 12-inch 里,当 consoleViewController hitTest return nil 后,事件会错误地传递给 consoleViewController.view.superview(而不是 consoleWindow),不清楚原因,暂时做一下保护 + // https://github.com/Tencent/QMUI_iOS/issues/1169 + UIView *originalView = [super hitTest:point withEvent:event]; + return originalView == self || originalView == self.rootViewController.view.superview ? nil : originalView; +} + +@end + @interface QMUIConsole () -@property(nonatomic, strong) UIWindow *consoleWindow; +@property(nonatomic, strong) QMUIConsoleWindow *consoleWindow; @property(nonatomic, strong) QMUIConsoleViewController *consoleViewController; @end @@ -66,6 +93,7 @@ + (id)allocWithZone:(struct _NSZone *)zone{ + (void)logWithLevel:(NSString *)level name:(NSString *)name logString:(id)logString { QMUIConsole *console = [QMUIConsole sharedInstance]; + if (!QMUIConsole.sharedInstance.canShow) return; [console initConsoleWindowIfNeeded]; [console.consoleViewController logWithLevel:level name:name logString:logString]; if (console.showConsoleAutomatically) { @@ -84,8 +112,19 @@ + (void)clear { + (void)show { QMUIConsole *console = [QMUIConsole sharedInstance]; if (console.canShow) { - [[QMUIConsole sharedInstance] initConsoleWindowIfNeeded]; - [QMUIConsole sharedInstance].consoleWindow.hidden = NO; + + if (!console.consoleWindow.hidden) return; + + // 在某些情况下 show 的时候刚好界面正在做动画,就可能会看到 consoleWindow 从左上角展开的过程(window 默认背景色是黑色的),所以这里做了一些小处理 + // https://github.com/Tencent/QMUI_iOS/issues/743 + [UIView performWithoutAnimation:^{ + [console initConsoleWindowIfNeeded]; + console.consoleWindow.alpha = 0; + console.consoleWindow.hidden = NO; + }]; + [UIView animateWithDuration:.25 delay:.2 options:QMUIViewAnimationOptionsCurveOut animations:^{ + console.consoleWindow.alpha = 1; + } completion:nil]; } } @@ -95,14 +134,7 @@ + (void)hide { - (void)initConsoleWindowIfNeeded { if (!self.consoleWindow) { - self.consoleWindow = [[UIWindow alloc] init]; - self.consoleWindow.backgroundColor = nil; - self.consoleWindow.qmui_capturesStatusBarAppearance = NO; - __weak __typeof(self)weakSelf = self; - self.consoleWindow.qmui_hitTestBlock = ^__kindof UIView * _Nonnull(CGPoint point, UIEvent * _Nonnull event, __kindof UIView * _Nonnull originalView) { - return originalView == weakSelf.consoleWindow ? nil : originalView; - }; - + self.consoleWindow = [[QMUIConsoleWindow alloc] init]; self.consoleViewController = [[QMUIConsoleViewController alloc] init]; self.consoleWindow.rootViewController = self.consoleViewController; } diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.h b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.h index b82e830e..8419c765 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.h @@ -1,17 +1,16 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIConsoleToolbar.h // QMUIKit // // Created by MoLice on 2019/J/11. -// Copyright © 2019 QMUI Team. All rights reserved. // #import diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.m b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.m index 0c172d13..c8958fff 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.m @@ -1,17 +1,16 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIConsoleToolbar.m // QMUIKit // // Created by MoLice on 2019/J/11. -// Copyright © 2019 QMUI Team. All rights reserved. // #import "QMUIConsoleToolbar.h" @@ -23,8 +22,8 @@ #import "UIImage+QMUI.h" #import "UIView+QMUI.h" #import "UIColor+QMUI.h" -#import "UIControl+QMUI.h" #import "UIImage+QMUI.h" +#import "UIControl+QMUI.h" @interface QMUIConsoleToolbar () @@ -88,6 +87,7 @@ - (instancetype)initWithFrame:(CGRect)frame { [self.searchRightView addSubview:self.searchResultCountLabel]; _searchResultPreviousButton = [[QMUIButton alloc] init]; + self.searchResultPreviousButton.qmui_preventsRepeatedTouchUpInsideEvent = NO; [self.searchResultPreviousButton setTitle:@"<" forState:UIControlStateNormal]; self.searchResultPreviousButton.titleLabel.font = UIFontMake(12); [self.searchResultPreviousButton setTitleColor:self.searchTextField.textColor forState:UIControlStateNormal]; @@ -95,6 +95,7 @@ - (instancetype)initWithFrame:(CGRect)frame { [self.searchRightView addSubview:self.searchResultPreviousButton]; _searchResultNextButton = [[QMUIButton alloc] init]; + self.searchResultNextButton.qmui_preventsRepeatedTouchUpInsideEvent = NO; [self.searchResultNextButton setTitle:@">" forState:UIControlStateNormal]; self.searchResultNextButton.titleLabel.font = UIFontMake(12); [self.searchResultNextButton setTitleColor:self.searchTextField.textColor forState:UIControlStateNormal]; @@ -112,8 +113,8 @@ - (void)layoutSubviews { UIEdgeInsets paddings = UIEdgeInsetsMake(8, 8, 8, 8); - CGFloat x = paddings.left + self.qmui_safeAreaInsets.left; - CGFloat contentHeight = CGRectGetHeight(self.bounds) - self.qmui_safeAreaInsets.bottom - UIEdgeInsetsGetVerticalValue(paddings); + CGFloat x = paddings.left + self.safeAreaInsets.left; + CGFloat contentHeight = CGRectGetHeight(self.bounds) - self.safeAreaInsets.bottom - UIEdgeInsetsGetVerticalValue(paddings); self.levelButton.frame = CGRectMake(x, paddings.top, contentHeight, contentHeight); x = CGRectGetMaxX(self.levelButton.frame); @@ -121,7 +122,7 @@ - (void)layoutSubviews { self.nameButton.frame = CGRectSetX(self.levelButton.frame, CGRectGetMaxX(self.levelButton.frame)); x = CGRectGetMaxX(self.nameButton.frame); - self.clearButton.frame = CGRectSetX(self.levelButton.frame, CGRectGetWidth(self.bounds) - self.qmui_safeAreaInsets.right - paddings.right - contentHeight); + self.clearButton.frame = CGRectSetX(self.levelButton.frame, CGRectGetWidth(self.bounds) - self.safeAreaInsets.right - paddings.right - contentHeight); CGFloat searchTextFieldMarginHorizontal = 8; CGFloat searchTextFieldMinX = x + searchTextFieldMarginHorizontal; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.h b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.h index 293a05e9..7c264272 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.h @@ -1,9 +1,15 @@ +/** + * 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. + */ // // QMUIConsoleViewController.h // QMUIKit // // Created by MoLice on 2019/J/11. -// Copyright © 2019 QMUI Team. All rights reserved. // #import @@ -13,13 +19,13 @@ NS_ASSUME_NONNULL_BEGIN @class QMUIButton; -@class QMUITextView; +@class QMUITableView; @class QMUIConsoleToolbar; @interface QMUIConsoleViewController : QMUICommonViewController @property(nonatomic, strong, readonly) QMUIButton *popoverButton; -@property(nonatomic, strong, readonly) QMUITextView *textView; +@property(nonatomic, strong, readonly) QMUITableView *tableView; @property(nonatomic, strong, readonly) QMUIConsoleToolbar *toolbar; @property(nonatomic, strong, readonly) NSDateFormatter *dateFormatter; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m index 13552e60..696aff20 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m @@ -1,13 +1,22 @@ +/** + * 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. + */ // // QMUIConsoleViewController.m // QMUIKit // // Created by MoLice on 2019/J/11. -// Copyright © 2019 QMUI Team. All rights reserved. // #import "QMUIConsoleViewController.h" #import "QMUICore.h" +#import "QMUITableView.h" +#import "QMUITableViewCell.h" +#import "UITableView+QMUICellHeightKeyCache.h" #import "QMUITextView.h" #import "QMUITextField.h" #import "QMUIButton.h" @@ -17,6 +26,7 @@ #import "UIImage+QMUI.h" #import "NSObject+QMUI.h" #import "CAAnimation+QMUI.h" +#import "NSArray+QMUI.h" #import "QMUIConsole.h" #import "QMUIPopupMenuView.h" @@ -26,12 +36,18 @@ @interface QMUIConsoleLogItem : NSObject @property(nullable, nonatomic, copy) NSString *name; @property(nonatomic, copy) NSAttributedString *timeString; @property(nonatomic, copy) NSAttributedString *logString; +@property(nonatomic, copy) NSAttributedString *displayString; + +@property(nonatomic, copy) NSString *searchingString; +@property(nonatomic, copy) NSArray *searchResults; +- (void)updateDisplayStringWithSearchResults:(NSArray *)searchResults; +- (void)focusSearchResultAtIndex:(NSInteger)index; @end @implementation QMUIConsoleLogItem + (instancetype)logItemWithLevel:(NSString *)level name:(NSString *)name timeString:(NSString *)timeString logString:(id)logString { - QMUIConsoleLogItem *logItem = [[QMUIConsoleLogItem alloc] init]; + QMUIConsoleLogItem *logItem = [[self alloc] init]; logItem.level = level ?: @"Normal"; logItem.name = name ?: @"Default"; @@ -49,21 +65,80 @@ + (instancetype)logItemWithLevel:(NSString *)level name:(NSString *)name timeStr } logItem.logString = string; + NSMutableAttributedString *displayString = NSMutableAttributedString.new; + [displayString appendAttributedString:logItem.timeString]; + [displayString appendAttributedString:logItem.logString]; + logItem.displayString = displayString; return logItem; } +- (void)updateDisplayStringWithSearchResults:(NSArray *)searchResults { + self.searchResults = searchResults; + NSMutableAttributedString *displayString = self.displayString.mutableCopy; + [displayString removeAttribute:NSBackgroundColorAttributeName range:NSMakeRange(0, displayString.length)]; + [searchResults enumerateObjectsUsingBlock:^(NSTextCheckingResult * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [displayString addAttribute:NSBackgroundColorAttributeName value:[[QMUIConsole appearance].searchResultHighlightedBackgroundColor colorWithAlphaComponent:.4] range:NSMakeRange(self.timeString.length + obj.range.location, obj.range.length)]; + }]; + self.displayString = displayString.copy; +} + +- (void)focusSearchResultAtIndex:(NSInteger)index { + NSAssert(index < self.searchResults.count, @"尝试聚焦一个超出 searchResults 范围的关键词"); + [self updateDisplayStringWithSearchResults:self.searchResults];// 重置之前的 focus range + NSRange rangeInLogString = self.searchResults[index].range; + NSRange range = NSMakeRange(self.timeString.length + rangeInLogString.location, rangeInLogString.length); + NSMutableAttributedString *displayString = self.displayString.mutableCopy; + [displayString addAttribute:NSBackgroundColorAttributeName value:[QMUIConsole appearance].searchResultHighlightedBackgroundColor range:range]; + self.displayString = displayString.copy; +} + @end -@interface QMUIConsoleViewController () +@interface QMUIConsoleLogItemCell : QMUITableViewCell + +@property(nonatomic, strong) QMUITextView *textView; +@end + +@implementation QMUIConsoleLogItemCell + +- (void)didInitializeWithStyle:(UITableViewCellStyle)style { + [super didInitializeWithStyle:style]; + self.backgroundColor = UIColor.clearColor; + self.selectionStyle = UITableViewCellSelectionStyleNone; + + self.textView = [[QMUITextView alloc] init]; + self.textView.textContainerInset = UIEdgeInsetsMake(2, 0, 2, 0); + self.textView.backgroundColor = [UIColor clearColor]; + self.textView.scrollsToTop = NO; + self.textView.editable = NO; + self.textView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + [self.contentView addSubview:self.textView]; +} + +- (CGSize)sizeThatFits:(CGSize)size { + return [self.textView sizeThatFits:CGSizeMake(size.width, CGFLOAT_MAX)]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + self.textView.frame = self.contentView.bounds; +} + +@end + +@interface QMUIConsoleViewController () @property(nonatomic, strong) UIView *containerView; @property(nonatomic, strong) QMUIPopupMenuView *levelMenu; @property(nonatomic, strong) QMUIPopupMenuView *nameMenu; @property(nonatomic, strong) NSMutableArray *logItems; +@property(nonatomic, strong) NSArray *showingLogItems; @property(nonatomic, strong) NSMutableArray *selectedLevels; @property(nonatomic, strong) NSMutableArray *selectedNames; -@property(nonatomic, copy) NSArray *searchResults; +@property(nonatomic, strong) NSRegularExpression *searchRegularExpression; +@property(nonatomic, assign) NSInteger searchResultsTotalCount; @property(nonatomic, assign) NSInteger currentHighlightedResultIndex; +@property(nonatomic, weak) QMUIConsoleLogItem *lastHighlightedItem; @property(nonatomic, strong) UIPanGestureRecognizer *popoverPanGesture; @property(nonatomic, strong) UILongPressGestureRecognizer *popoverLongPressGesture; @@ -74,7 +149,6 @@ @implementation QMUIConsoleViewController - (void)didInitialize { [super didInitialize]; - self.automaticallyAdjustsScrollViewInsets = NO; self.backgroundColor = [QMUIConsole appearance].backgroundColor; _dateFormatter = [[NSDateFormatter alloc] init]; @@ -83,48 +157,90 @@ - (void)didInitialize { self.logItems = [[NSMutableArray alloc] init]; self.selectedLevels = [[NSMutableArray alloc] init]; self.selectedNames = [[NSMutableArray alloc] init]; - - [self loadViewIfNeeded]; +} + +- (UIView *)containerView { + if (!_containerView) { + _containerView = [[UIView alloc] init]; + _containerView.backgroundColor = self.backgroundColor; + _containerView.hidden = YES; + } + return _containerView; +} + +@synthesize tableView = _tableView; +- (QMUITableView *)tableView { + if (!_tableView) { + _tableView = [[QMUITableView alloc] init]; + _tableView.dataSource = self; + _tableView.delegate = self; + _tableView.estimatedRowHeight = 44; + _tableView.rowHeight = UITableViewAutomaticDimension; + _tableView.qmui_cacheCellHeightByKeyAutomatically = YES; + _tableView.backgroundColor = nil; + _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + _tableView.scrollsToTop = NO; + [_tableView registerClass:QMUIConsoleLogItemCell.class forCellReuseIdentifier:@"cell"]; + _tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } + return _tableView; +} + +@synthesize toolbar = _toolbar; +- (QMUIConsoleToolbar *)toolbar { + if (!_toolbar) { + _toolbar = [[QMUIConsoleToolbar alloc] init]; + [_toolbar.levelButton addTarget:self action:@selector(handleLevelButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [_toolbar.nameButton addTarget:self action:@selector(handleNameButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [_toolbar.clearButton addTarget:self action:@selector(handleClearButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + __weak __typeof(self)weakSelf = self; + _toolbar.searchTextField.qmui_keyboardWillChangeFrameNotificationBlock = ^(QMUIKeyboardUserInfo *keyboardUserInfo) { + [weakSelf handleKeyboardWillChangeFrame:keyboardUserInfo]; + }; + _toolbar.searchTextField.delegate = self; + [_toolbar.searchTextField addTarget:self action:@selector(handleSearchTextFieldChanged:) forControlEvents:UIControlEventEditingChanged]; + [_toolbar.searchResultPreviousButton addTarget:self action:@selector(handleSearchResultPreviousButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [_toolbar.searchResultNextButton addTarget:self action:@selector(handleSearchResultNextButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + } + return _toolbar; +} + +@synthesize popoverButton = _popoverButton; +- (QMUIButton *)popoverButton { + if (!_popoverButton) { + UIImage *popoverImage = [[QMUIHelper imageWithName:@"QMUI_console_logo"] qmui_imageResizedInLimitedSize:CGSizeMake(24, 24)]; + _popoverButton = [[QMUIButton alloc] qmui_initWithSize:CGSizeMake(32, 32)]; + [_popoverButton setImage:popoverImage forState:UIControlStateNormal]; + _popoverButton.adjustsButtonWhenHighlighted = NO; + _popoverButton.backgroundColor = [[QMUIConsole appearance].backgroundColor colorWithAlphaComponent:.5]; + _popoverButton.layer.cornerRadius = CGRectGetHeight(_popoverButton.bounds) / 2; + _popoverButton.clipsToBounds = YES; + [_popoverButton addTarget:self action:@selector(handlePopoverTouchEvent:) forControlEvents:UIControlEventTouchUpInside]; + + self.popoverLongPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePopverLongPressGestureRecognizer:)]; + [_popoverButton addGestureRecognizer:self.popoverLongPressGesture]; + + self.popoverPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePopoverPanGestureRecognizer:)]; + [self.popoverPanGesture requireGestureRecognizerToFail:self.popoverLongPressGesture]; + [_popoverButton addGestureRecognizer:self.popoverPanGesture]; + } + return _popoverButton; } - (void)initSubviews { [super initSubviews]; - - self.containerView = [[UIView alloc] init]; - self.containerView.backgroundColor = self.backgroundColor; - self.containerView.hidden = YES; [self.view addSubview:self.containerView]; - - _textView = [[QMUITextView alloc] init]; - self.textView.backgroundColor = [UIColor clearColor]; - self.textView.scrollsToTop = NO; - self.textView.editable = NO; - if (@available(iOS 11, *)) { - self.textView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } - [self.containerView addSubview:self.textView]; - - _toolbar = [[QMUIConsoleToolbar alloc] init]; - [self.toolbar.levelButton addTarget:self action:@selector(handleLevelButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self.toolbar.nameButton addTarget:self action:@selector(handleNameButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self.toolbar.clearButton addTarget:self action:@selector(handleClearButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - __weak __typeof(self)weakSelf = self; - self.toolbar.searchTextField.qmui_keyboardWillChangeFrameNotificationBlock = ^(QMUIKeyboardUserInfo *keyboardUserInfo) { - [weakSelf handleKeyboardWillChangeFrame:keyboardUserInfo]; - }; - self.toolbar.searchTextField.delegate = self; - [self.toolbar.searchTextField addTarget:self action:@selector(handleSearchTextFieldChanged:) forControlEvents:UIControlEventEditingChanged]; - [self.toolbar.searchResultPreviousButton addTarget:self action:@selector(handleSearchResultPreviousButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self.toolbar.searchResultNextButton addTarget:self action:@selector(handleSearchResultNextButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.containerView addSubview:self.tableView]; [self.containerView addSubview:self.toolbar]; - self.levelMenu = [self generatePopupMenuView]; + __weak __typeof(self)weakSelf = self; + self.levelMenu = [self generatePopupMenuViewWithSelectedArray:self.selectedLevels]; self.levelMenu.willHideBlock = ^(BOOL hidesByUserTap, BOOL animated) { weakSelf.toolbar.levelButton.selected = weakSelf.selectedLevels.count > 0; }; self.levelMenu.sourceView = self.toolbar.levelButton; - self.nameMenu = [self generatePopupMenuView]; + self.nameMenu = [self generatePopupMenuViewWithSelectedArray:self.selectedNames]; self.nameMenu.willHideBlock = ^(BOOL hidesByUserTap, BOOL animated) { weakSelf.toolbar.nameButton.selected = weakSelf.selectedNames.count > 0; }; @@ -132,19 +248,6 @@ - (void)initSubviews { [self updateToolbarButtonState]; - UIImage *popoverImage = [[QMUIHelper imageWithName:@"QMUI_console_logo"] qmui_imageResizedInLimitedSize:CGSizeMake(24, 24)]; - _popoverButton = [[QMUIButton alloc] qmui_initWithSize:CGSizeMake(32, 32)]; - [self.popoverButton setImage:popoverImage forState:UIControlStateNormal]; - self.popoverButton.adjustsButtonWhenHighlighted = NO; - self.popoverButton.backgroundColor = [[QMUIConsole appearance].backgroundColor colorWithAlphaComponent:.5]; - self.popoverButton.layer.cornerRadius = CGRectGetHeight(self.popoverButton.bounds) / 2; - self.popoverButton.clipsToBounds = YES; - [self.popoverButton addTarget:self action:@selector(handlePopoverTouchEvent:) forControlEvents:UIControlEventTouchUpInside]; - self.popoverLongPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePopverLongPressGestureRecognizer:)]; - [self.popoverButton addGestureRecognizer:self.popoverLongPressGesture]; - self.popoverPanGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePopoverPanGestureRecognizer:)]; - [self.popoverPanGesture requireGestureRecognizerToFail:self.popoverLongPressGesture]; - [self.popoverButton addGestureRecognizer:self.popoverPanGesture]; [self.view addSubview:self.popoverButton]; } @@ -152,7 +255,7 @@ - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor clearColor]; __weak __typeof(self)weakSelf = self; - self.view.qmui_hitTestBlock = ^__kindof UIView * _Nonnull(CGPoint point, UIEvent * _Nonnull event, __kindof UIView * _Nonnull originalView) { + self.view.qmui_hitTestBlock = ^__kindof UIView * _Nullable(CGPoint point, UIEvent * _Nullable event, __kindof UIView * _Nullable originalView) { QMUIPopupMenuView *menuView = weakSelf.levelMenu.isShowing ? weakSelf.levelMenu : (weakSelf.nameMenu.isShowing ? weakSelf.nameMenu : nil); if (menuView && ![originalView isDescendantOfView:menuView]) { @@ -171,10 +274,10 @@ - (void)viewDidLoad { } - (CGRect)safetyPopoverButtonFrame:(CGRect)popoverButtonFrame { - CGRect safetyBounds = CGRectInsetEdges(self.view.bounds, self.view.qmui_safeAreaInsets); + CGRect safetyBounds = CGRectInsetEdges(self.view.bounds, self.view.safeAreaInsets); if (!CGRectContainsRect(safetyBounds, self.popoverButton.frame)) { - popoverButtonFrame = CGRectSetX(popoverButtonFrame, MAX(self.view.qmui_safeAreaInsets.left, MIN(CGRectGetMaxX(safetyBounds) - CGRectGetWidth(popoverButtonFrame), CGRectGetMinX(popoverButtonFrame)))); - popoverButtonFrame = CGRectSetY(popoverButtonFrame, MAX(self.view.qmui_safeAreaInsets.top, MIN(CGRectGetMaxY(safetyBounds) - CGRectGetHeight(popoverButtonFrame), CGRectGetMinY(popoverButtonFrame)))); + popoverButtonFrame = CGRectSetX(popoverButtonFrame, MAX(self.view.safeAreaInsets.left, MIN(CGRectGetMaxX(safetyBounds) - CGRectGetWidth(popoverButtonFrame), CGRectGetMinX(popoverButtonFrame)))); + popoverButtonFrame = CGRectSetY(popoverButtonFrame, MAX(self.view.safeAreaInsets.top, MIN(CGRectGetMaxY(safetyBounds) - CGRectGetHeight(popoverButtonFrame), CGRectGetMinY(popoverButtonFrame)))); } return popoverButtonFrame; } @@ -186,7 +289,7 @@ - (void)layoutPopoverButton { if (bindObject) { popoverButtonOrigin = ((NSValue *)[self.popoverButton qmui_getBoundObjectForKey:@"origin"]).CGPointValue; } else { - popoverButtonOrigin = CGPointMake(16 + self.view.qmui_safeAreaInsets.left, CGRectGetHeight(self.view.bounds) * 3.0 / 4.0); + popoverButtonOrigin = CGPointMake(16 + self.view.safeAreaInsets.left, CGRectGetHeight(self.view.bounds) * 3.0 / 4.0); } self.popoverButton.qmui_frameApplyTransform = [self safetyPopoverButtonFrame:CGRectSetXY(self.popoverButton.frame, popoverButtonOrigin.x, popoverButtonOrigin.y)]; } else { @@ -204,18 +307,18 @@ - (void)viewDidLayoutSubviews { CGSize containerViewSize = CGSizeMake(CGRectGetWidth(self.view.bounds), MAX(100, CGRectGetHeight(self.view.bounds) / 3)); self.containerView.qmui_frameApplyTransform = CGRectMake(0, CGRectGetHeight(self.view.bounds) - containerViewSize.height, containerViewSize.width, containerViewSize.height); - CGFloat toolbarHeight = 44 + self.containerView.qmui_safeAreaInsets.bottom; + CGFloat toolbarHeight = 44 + self.containerView.safeAreaInsets.bottom; self.toolbar.qmui_height = toolbarHeight; self.toolbar.qmui_width = self.containerView.qmui_width; self.toolbar.qmui_bottom = self.containerView.qmui_height; - self.textView.qmui_width = self.containerView.qmui_width; - self.textView.qmui_height = self.toolbar.qmui_top; - self.textView.contentInset = UIEdgeInsetsMake(self.textView.qmui_safeAreaInsets.top, self.textView.qmui_safeAreaInsets.left, self.textView.contentInset.bottom, self.textView.qmui_safeAreaInsets.right); - self.textView.scrollIndicatorInsets = self.textView.contentInset; + self.tableView.qmui_width = self.containerView.qmui_width; + self.tableView.qmui_height = self.toolbar.qmui_top; + self.tableView.contentInset = UIEdgeInsetsMake(self.tableView.safeAreaInsets.top, self.tableView.safeAreaInsets.left, self.tableView.contentInset.bottom, self.tableView.safeAreaInsets.right); + self.tableView.scrollIndicatorInsets = self.tableView.contentInset; [@[self.levelMenu, self.nameMenu] enumerateObjectsUsingBlock:^(QMUIPopupMenuView *menuView, NSUInteger idx, BOOL * _Nonnull stop) { - menuView.safetyMarginsOfSuperview = UIEdgeInsetsConcat(UIEdgeInsetsMake(2, 2, 2, 2), self.view.qmui_safeAreaInsets); + menuView.safetyMarginsOfSuperview = UIEdgeInsetsConcat(UIEdgeInsetsMake(2, 2, 2, 2), self.view.safeAreaInsets); }]; } @@ -236,9 +339,11 @@ - (void)setBackgroundColor:(UIColor *)backgroundColor { - (void)logWithLevel:(NSString *)level name:(NSString *)name logString:(id)logString { QMUIConsoleLogItem *logItem = [QMUIConsoleLogItem logItemWithLevel:level name:name timeString:[self.dateFormatter stringFromDate:[NSDate new]] logString:logString]; + [self searchInLogItem:logItem]; [self.logItems addObject:logItem]; - [self updateToolbarButtonState]; - [self printLog]; + dispatch_async(dispatch_get_main_queue(), ^{// 避免频繁打 log 时卡顿 + [self printLog]; + }); } - (void)log:(id)logString { @@ -246,24 +351,46 @@ - (void)log:(id)logString { } - (void)printLog { - NSMutableAttributedString *string = [[NSMutableAttributedString alloc] init]; - [self.logItems enumerateObjectsUsingBlock:^(QMUIConsoleLogItem * _Nonnull logItem, NSUInteger idx, BOOL * _Nonnull stop) { + self.showingLogItems = [self.logItems qmui_filterWithBlock:^BOOL(QMUIConsoleLogItem * _Nonnull logItem) { BOOL shouldPrintLevel = !self.selectedLevels.count || [self.selectedLevels containsObject:logItem.level]; BOOL shouldPrintName = !self.selectedNames.count || [self.selectedNames containsObject:logItem.name]; - if (shouldPrintLevel && shouldPrintName) { - if (string.length > 0) { - [string appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n" attributes:[QMUIConsole appearance].textAttributes]]; - } - - [string appendAttributedString:logItem.timeString]; - [string appendAttributedString:logItem.logString]; - } + return shouldPrintLevel && shouldPrintName; }]; - self.textView.attributedText = string; - if (self.toolbar.searchTextField.text.length > 0) { - [self handleSearchTextFieldChanged:self.toolbar.searchTextField]; - } else { - [self.textView qmui_scrollToBottomAnimated:YES]; + if (_tableView) { + [self updateToolbarButtonState]; + + [self.tableView reloadData]; + [self.tableView performBatchUpdates:^{ + } completion:^(BOOL finished) { + NSArray *matchedItems = [self.showingLogItems qmui_filterWithBlock:^BOOL(QMUIConsoleLogItem * _Nonnull item) { + return item.searchResults.count > 0; + }]; + NSArray *> *matchedResults = [matchedItems qmui_mapWithBlock:^id _Nonnull(QMUIConsoleLogItem * _Nonnull item, NSInteger index) { + return item.searchResults; + }]; + self.searchResultsTotalCount = 0; + [matchedResults enumerateObjectsUsingBlock:^(NSArray * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + self.searchResultsTotalCount += obj.count; + }]; + + BOOL shouldShowCountLabel = self.toolbar.searchTextField.text.length > 0;// 不管有没有结果,只要有搜索文本,就显示结果计数 + if (shouldShowCountLabel) { + self.toolbar.searchTextField.rightViewMode = UITextFieldViewModeAlways; + self.toolbar.searchResultPreviousButton.enabled = self.searchResultsTotalCount > 1; + self.toolbar.searchResultNextButton.enabled = self.searchResultsTotalCount > 1; + } else { + self.toolbar.searchTextField.rightViewMode = UITextFieldViewModeNever; + } + if (self.searchResultsTotalCount == 0) { + self.currentHighlightedResultIndex = -1;// < 0 时不会自动滚动,所以需要手动再滚到列表末尾 + if ([self.tableView numberOfRowsInSection:0] > 0) { + NSIndexPath *lastRow = [NSIndexPath indexPathForRow:[self.tableView numberOfRowsInSection:0] - 1 inSection:0]; + [self.tableView scrollToRowAtIndexPath:lastRow atScrollPosition:UITableViewScrollPositionMiddle animated:YES]; + } + } else { + self.currentHighlightedResultIndex = 0;// >= 0 时内部会自动滚动 + } + }]; } } @@ -275,9 +402,25 @@ - (void)clear { self.toolbar.levelButton.selected = NO; self.toolbar.nameButton.enabled = NO; self.toolbar.nameButton.selected = NO; - self.textView.attributedText = nil; - self.searchResults = nil; - [self handleSearchTextFieldChanged:self.toolbar.searchTextField]; + [self printLog]; +} + +#pragma mark - + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.showingLogItems.count; +} + +- (id)qmui_tableView:(UITableView *)tableView cacheKeyForRowAtIndexPath:(NSIndexPath *)indexPath { + return self.showingLogItems[indexPath.row].logString.string; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + QMUIConsoleLogItemCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath]; + QMUIConsoleLogItem *logItem = self.showingLogItems[indexPath.row]; + cell.textView.attributedText = logItem.displayString.copy; + [cell updateCellAppearanceWithIndexPath:indexPath]; + return cell; } #pragma mark - Popover Button @@ -385,9 +528,7 @@ - (void)handlePopverLongPressGestureRecognizer:(UILongPressGestureRecognizer *)g scale.qmui_animationDidStopBlock = ^(__kindof CAAnimation *aAnimation, BOOL finished) { [QMUIConsole hide]; [weakSelf.popoverButton.layer removeAnimationForKey:@"scale"]; - if (@available(iOS 10.0, *)) { - [[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight] impactOccurred]; - } + [[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium] impactOccurred]; }; [self.popoverButton.layer addAnimation:scale forKey:@"scale"]; } @@ -400,52 +541,52 @@ - (void)updateToolbarButtonState { self.toolbar.nameButton.enabled = self.logItems.count > 0; } -- (QMUIPopupMenuView *)generatePopupMenuView { +- (QMUIPopupMenuView *)generatePopupMenuViewWithSelectedArray:(NSArray *)selectedArray { QMUIPopupMenuView *menuView = [[QMUIPopupMenuView alloc] init]; menuView.padding = UIEdgeInsetsMake(3, 6, 3, 6); menuView.cornerRadius = 3; menuView.arrowSize = CGSizeMake(8, 4); menuView.borderWidth = 0; menuView.itemHeight = 28; - menuView.itemTitleFont = UIFontMake(12); - menuView.itemTitleColor = UIColorMake(53, 60, 70); menuView.maskViewBackgroundColor = nil; menuView.backgroundColor = UIColorWhite; - menuView.itemConfigurationHandler = ^(QMUIPopupMenuView *aMenuView, QMUIPopupMenuButtonItem *aItem, NSInteger section, NSInteger index) { - aItem.highlightedBackgroundColor = [[UIColor blackColor] colorWithAlphaComponent:.15]; - QMUIButton *button = aItem.button; + menuView.itemViewConfigurationHandler = ^(QMUIPopupMenuView * _Nonnull aMenuView, __kindof QMUIPopupMenuItem * _Nonnull aItem, QMUIPopupMenuItemView * _Nonnull aItemView, NSInteger section, NSInteger index) { + aItemView.button.highlightedBackgroundColor = [[UIColor blackColor] colorWithAlphaComponent:.15]; + QMUIButton *button = aItemView.button; + button.titleLabel.font = UIFontMake(12); + button.tintColorAdjustsTitleAndImage = UIColorMake(53, 60, 70); button.imagePosition = QMUIButtonImagePositionRight; button.spacingBetweenImageAndTitle = 10; - UIImage *selectedImage = [UIImage qmui_imageWithShape:QMUIImageShapeCheckmark size:CGSizeMake(12, 9) lineWidth:1 tintColor:aMenuView.itemTitleColor]; + UIImage *selectedImage = [[UIImage qmui_imageWithShape:QMUIImageShapeCheckmark size:CGSizeMake(12, 9) lineWidth:1 tintColor:UIColor.blackColor] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; UIImage *normalImage = [UIImage qmui_imageWithColor:UIColorClear size:selectedImage.size cornerRadius:0]; [button setImage:normalImage forState:UIControlStateNormal];// 无图像也弄一张空白图,以保证 state 变化时布局不跳动 [button setImage:selectedImage forState:UIControlStateSelected]; [button setImage:selectedImage forState:UIControlStateSelected|UIControlStateHighlighted]; + button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill; + button.selected = [selectedArray containsObject:aItem.title]; }; menuView.hidden = YES; [self.view addSubview:menuView]; return menuView; } -- (NSArray *)popupMenuItemsByTitleBlock:(nullable NSString * (^)(QMUIConsoleLogItem *logItem))titleBlock selectedArray:(NSMutableArray *)selectedArray { +- (NSArray *)popupMenuItemsByTitleBlock:(nullable NSString * (^)(QMUIConsoleLogItem *logItem))titleBlock selectedArray:(NSMutableArray *)selectedArray { __weak __typeof(self)weakSelf = self; - NSMutableArray *items = [[NSMutableArray alloc] init]; + NSMutableArray *items = [[NSMutableArray alloc] init]; NSMutableSet *itemTitles = [[NSMutableSet alloc] init]; [self.logItems enumerateObjectsUsingBlock:^(QMUIConsoleLogItem * _Nonnull logItem, NSUInteger idx, BOOL * _Nonnull stop) { [itemTitles addObject:titleBlock(logItem)]; }]; [[itemTitles sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:NSStringFromSelector(@selector(description)) ascending:YES]]] enumerateObjectsUsingBlock:^(NSString * _Nonnull title, NSUInteger idx, BOOL * _Nonnull stop) { - QMUIPopupMenuButtonItem *item = [QMUIPopupMenuButtonItem itemWithImage:nil title:title handler:^(QMUIPopupMenuButtonItem *aItem) { - aItem.button.selected = !aItem.button.selected; - if (aItem.button.selected) { + QMUIPopupMenuItem *item = [QMUIPopupMenuItem itemWithTitle:title handler:^(__kindof QMUIPopupMenuItem * _Nonnull aItem, QMUIPopupMenuItemView * _Nonnull aItemView, NSInteger section, NSInteger index) { + aItemView.button.selected = !aItemView.button.selected; + if (aItemView.button.selected) { [selectedArray addObject:title]; } else { [selectedArray removeObject:title]; } [weakSelf printLog]; }]; - item.button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill; - item.button.selected = [selectedArray containsObject:title]; [items addObject:item]; }]; return items.copy; @@ -473,64 +614,77 @@ - (void)handleClearButtonEvent:(UIButton *)button { #pragma mark - Search +- (void)searchInLogItem:(QMUIConsoleLogItem *)logItem { + NSString *searchingText = self.toolbar.searchTextField.text ?: @""; + BOOL valueChanged = ![searchingText isEqualToString:logItem.searchingString ?: @""];// UITextField.text 不会为 nil,至少是 @"",为了保证 isEqualToString: 的正确性,这里对 searchingString 也做了 nil -> @"" 的转换 + if (!valueChanged) return; + logItem.searchingString = searchingText; + NSArray *matches = [self.searchRegularExpression matchesInString:logItem.logString.string options:NSMatchingReportCompletion range:NSMakeRange(0, logItem.logString.string.length)]; + [logItem updateDisplayStringWithSearchResults:matches]; +} + - (void)handleSearchTextFieldChanged:(QMUITextField *)searchTextField { if (self.levelMenu.isShowing) [self.levelMenu hideWithAnimated:YES]; if (self.nameMenu.isShowing) [self.nameMenu hideWithAnimated:YES]; - NSError *error = nil; - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:searchTextField.text options:NSRegularExpressionCaseInsensitive error:&error]; - - if (!error) { - NSString *text = self.textView.text; - self.searchResults = [regex matchesInString:text options:NSMatchingReportProgress range:NSMakeRange(0, text.length)]; - if (self.searchResults.count) { - self.currentHighlightedResultIndex = 0; - self.toolbar.searchResultPreviousButton.enabled = self.searchResults.count > 1; - self.toolbar.searchResultNextButton.enabled = self.searchResults.count > 1; - - self.toolbar.searchResultCountLabel.text = [NSString stringWithFormat:@"%@个结果", @(self.searchResults.count)]; - [self.toolbar setNeedsLayoutSearchResultViews]; - self.toolbar.searchTextField.rightViewMode = UITextFieldViewModeAlways; - [self updateSearchHighlighted]; - return; - } - } - - self.currentHighlightedResultIndex = -1; - self.toolbar.searchTextField.rightViewMode = UITextFieldViewModeNever; - [self updateSearchHighlighted]; + self.searchRegularExpression = [NSRegularExpression regularExpressionWithPattern:searchTextField.text options:NSRegularExpressionCaseInsensitive error:nil]; + [self.logItems enumerateObjectsUsingBlock:^(QMUIConsoleLogItem * _Nonnull logItem, NSUInteger idx, BOOL * _Nonnull stop) { + [self searchInLogItem:logItem]; + }]; + [self printLog]; } - (void)handleSearchResultPreviousButtonEvent:(QMUIButton *)button { if (self.currentHighlightedResultIndex == 0) { - self.currentHighlightedResultIndex = self.searchResults.count - 1; + self.currentHighlightedResultIndex = self.searchResultsTotalCount - 1; } else { self.currentHighlightedResultIndex --; } - [self updateSearchHighlighted]; } - (void)handleSearchResultNextButtonEvent:(QMUIButton *)button { - if (self.currentHighlightedResultIndex == self.searchResults.count - 1) { + if (self.currentHighlightedResultIndex == self.searchResultsTotalCount - 1) { self.currentHighlightedResultIndex = 0; } else { self.currentHighlightedResultIndex ++; } - [self updateSearchHighlighted]; } -- (void)updateSearchHighlighted { - NSMutableAttributedString *attributedText = self.textView.attributedText.mutableCopy; - [attributedText addAttribute:NSBackgroundColorAttributeName value:[UIColor clearColor] range:NSMakeRange(0, attributedText.length)]; - if (self.currentHighlightedResultIndex >= 0) { - [attributedText addAttribute:NSBackgroundColorAttributeName value:[QMUIConsole appearance].searchResultHighlightedBackgroundColor range:self.searchResults[self.currentHighlightedResultIndex].range]; - } - - self.textView.attributedText = attributedText; - - if (self.currentHighlightedResultIndex >= 0) { - [self.textView scrollRangeToVisible:self.searchResults[self.currentHighlightedResultIndex].range]; +- (void)setCurrentHighlightedResultIndex:(NSInteger)currentHighlightedResultIndex { + _currentHighlightedResultIndex = currentHighlightedResultIndex; + [self.lastHighlightedItem updateDisplayStringWithSearchResults:self.lastHighlightedItem.searchResults];// clear focus + self.toolbar.searchResultCountLabel.text = currentHighlightedResultIndex >= 0 ? [NSString stringWithFormat:@"%@/%@", @(currentHighlightedResultIndex + 1), @(self.searchResultsTotalCount)] : @"0"; + [self.toolbar setNeedsLayoutSearchResultViews]; + if (currentHighlightedResultIndex >= 0) { + + NSInteger row = NSNotFound; + NSInteger indexInItem = NSNotFound; + for (NSInteger i = 0, j = 0; i < self.showingLogItems.count; i++) { + if (self.currentHighlightedResultIndex < j + self.showingLogItems[i].searchResults.count) { + row = i; + indexInItem = self.currentHighlightedResultIndex - j; + break; + } + j += self.showingLogItems[i].searchResults.count; + } + if (row != NSNotFound) { + [self.showingLogItems[row] focusSearchResultAtIndex:indexInItem]; + [self.tableView reloadData]; + self.lastHighlightedItem = self.showingLogItems[row]; + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; + BOOL shouldScrollToVisible = ![self.tableView qmui_cellVisibleAtIndexPath:indexPath]; + if (!shouldScrollToVisible) { + // 本来就可视的,可能 cell 比较高,只露出屏幕一半,高亮的那个地方没露出来,这种要手动计算 + CGRect rect = [self.tableView rectForRowAtIndexPath:indexPath]; + if (!CGRectContainsRect(self.tableView.bounds, rect)) { + shouldScrollToVisible = YES; + } + } + if (shouldScrollToVisible) { + [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES]; + } + } } } diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.h b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.h index e6d13da6..5b1259ed 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.h @@ -1,17 +1,16 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILog+QMUIConsole.h // QMUIKit // // Created by MoLice on 2019/J/15. -// Copyright © 2019 QMUI Team. All rights reserved. // #import "QMUILog.h" diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.m b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.m index 6dc1090e..1eb1526f 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.m @@ -1,17 +1,16 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILog+QMUIConsole.m // QMUIKit // // Created by MoLice on 2019/J/15. -// Copyright © 2019 QMUI Team. All rights reserved. // #import "QMUILog+QMUIConsole.h" @@ -31,16 +30,23 @@ + (void)load { originSelectorIMP = (void (*)(id, SEL, const char *, int, const char *, QMUILogItem *))originalIMPProvider(); originSelectorIMP(selfObject, originCMD, file, line, func, logItem); - // avoid superclass - if (![selfObject isKindOfClass:originClass]) return; - if (!QMUICMIActivated || !ShouldPrintQMUIWarnLogToConsole) return; if (!logItem.enabled) return; if (logItem.level != QMUILogLevelWarn) return; - NSString *funcString = [NSString stringWithFormat:@"%s", func]; - NSString *defaultString = [NSString stringWithFormat:@"%@:%@ | %@", funcString, @(line), logItem]; - [QMUIConsole logWithLevel:logItem.levelDisplayString name:logItem.name logString:defaultString]; + void (^block)(void) = ^void(void) { + NSString *funcString = [NSString stringWithFormat:@"%s", func]; + NSString *defaultString = [NSString stringWithFormat:@"%@:%@ | %@", funcString, @(line), logItem]; + [QMUIConsole logWithLevel:logItem.levelDisplayString name:logItem.name logString:defaultString]; + }; + if (!NSThread.currentThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), ^{ + block(); + }); + } else { + block(); + } + }; }); }); diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.h b/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.h index f59f7114..e1396ef7 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIDialogViewController.h @@ -40,10 +40,18 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, assign) UIEdgeInsets dialogViewMargins UI_APPEARANCE_SELECTOR; @property(nonatomic, assign) CGFloat maximumContentViewWidth UI_APPEARANCE_SELECTOR; @property(nullable, nonatomic, strong) UIColor *backgroundColor UI_APPEARANCE_SELECTOR; + +/// 标题的 tintColor,当没有设置 titleLabelTextColor 和 subTitleLabelTextColor 的情况下,标题和副标题的颜色均会使用 titleTintColor,当 titleLabelTextColor 和 subTitleLabelTextColor 其中任何一个被设置了值时,则 titleTintColor 作为候选项使用(也即谁为 nil 才会用 titleTintColor 顶替,不为 nil 则不会用到 titleTintColor)。 +/// 默认为 nil @property(nullable, nonatomic, strong) UIColor *titleTintColor UI_APPEARANCE_SELECTOR; @property(nullable, nonatomic, strong) UIFont *titleLabelFont UI_APPEARANCE_SELECTOR; + +/// 主标题的文字颜色,当为 nil 时则会使用 titleView 的 tintColor 作为文字颜色 @property(nullable, nonatomic, strong) UIColor *titleLabelTextColor UI_APPEARANCE_SELECTOR; @property(nullable, nonatomic, strong) UIFont *subTitleLabelFont UI_APPEARANCE_SELECTOR; + +/// 副标题的文字颜色,当为 nil 时则会使用 titleView 的 tintColor 作为文字颜色 +/// @note 副标题可通过 dialog.titleView.subtitle 来设置 @property(nullable, nonatomic, strong) UIColor *subTitleLabelTextColor UI_APPEARANCE_SELECTOR; @property(nullable, nonatomic, strong) UIColor *headerSeparatorColor UI_APPEARANCE_SELECTOR; @property(nonatomic, assign) CGFloat headerViewHeight UI_APPEARANCE_SELECTOR; @@ -55,7 +63,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nullable, nonatomic, strong) UIColor *footerViewBackgroundColor UI_APPEARANCE_SELECTOR; @property(nullable, nonatomic, strong) UIColor *buttonBackgroundColor UI_APPEARANCE_SELECTOR; @property(nullable, nonatomic, strong) UIColor *buttonHighlightedBackgroundColor UI_APPEARANCE_SELECTOR; -@property(nullable, nonatomic, strong) NSDictionary *buttonTitleAttributes UI_APPEARANCE_SELECTOR; +@property(nullable, nonatomic, copy) NSDictionary *buttonTitleAttributes UI_APPEARANCE_SELECTOR; @property(nullable, nonatomic, strong, readonly) UIView *headerView; @property(nullable, nonatomic, strong, readonly) CALayer *headerViewSeparatorLayer; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.m index 95cba392..94b2c1dc 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIDialogViewController.m @@ -24,47 +24,41 @@ #import "UITableView+QMUI.h" #import "NSString+QMUI.h" #import "UIScrollView+QMUI.h" +#import "QMUIAppearance.h" @implementation QMUIDialogViewController (UIAppearance) -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self appearance]; - }); ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; } -static QMUIDialogViewController *dialogViewControllerAppearance; -+ (nonnull instancetype)appearance { ++ (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - if (!dialogViewControllerAppearance) { - dialogViewControllerAppearance = [[QMUIDialogViewController alloc] init]; - dialogViewControllerAppearance.cornerRadius = 6; - dialogViewControllerAppearance.dialogViewMargins = UIEdgeInsetsMake(20, 20, 20, 20); // 在 -didInitialize 里会适配 iPhone X 的 safeAreaInsets - dialogViewControllerAppearance.maximumContentViewWidth = [QMUIHelper screenSizeFor55Inch].width - UIEdgeInsetsGetHorizontalValue(dialogViewControllerAppearance.dialogViewMargins); - dialogViewControllerAppearance.backgroundColor = UIColorWhite; - dialogViewControllerAppearance.titleTintColor = UIColorBlack; - dialogViewControllerAppearance.titleLabelFont = UIFontMake(16); - dialogViewControllerAppearance.titleLabelTextColor = UIColorMake(53, 60, 70); - dialogViewControllerAppearance.subTitleLabelFont = UIFontMake(12); - dialogViewControllerAppearance.subTitleLabelTextColor = UIColorMake(133, 140, 150); - - dialogViewControllerAppearance.headerSeparatorColor = UIColorMake(222, 224, 226); - dialogViewControllerAppearance.headerViewHeight = 48; - dialogViewControllerAppearance.headerViewBackgroundColor = UIColorMake(244, 245, 247); - dialogViewControllerAppearance.contentViewMargins = UIEdgeInsetsZero; - dialogViewControllerAppearance.contentViewBackgroundColor = nil; - dialogViewControllerAppearance.footerSeparatorColor = UIColorMake(222, 224, 226); - dialogViewControllerAppearance.footerViewHeight = 48; - dialogViewControllerAppearance.footerViewBackgroundColor = nil; - - dialogViewControllerAppearance.buttonBackgroundColor = nil; - dialogViewControllerAppearance.buttonTitleAttributes = @{NSForegroundColorAttributeName: UIColorBlue}; - dialogViewControllerAppearance.buttonHighlightedBackgroundColor = [UIColorBlue colorWithAlphaComponent:.25]; - } + QMUIDialogViewController *dialogViewControllerAppearance = QMUIDialogViewController.appearance; + dialogViewControllerAppearance.cornerRadius = 6; + dialogViewControllerAppearance.dialogViewMargins = UIEdgeInsetsMake(20, 20, 20, 20); // 在 -didInitialize 里会适配 iPhone X 的 safeAreaInsets + dialogViewControllerAppearance.maximumContentViewWidth = [QMUIHelper screenSizeFor55Inch].width - UIEdgeInsetsGetHorizontalValue(dialogViewControllerAppearance.dialogViewMargins); + dialogViewControllerAppearance.backgroundColor = UIColorWhite; + dialogViewControllerAppearance.titleTintColor = nil; + dialogViewControllerAppearance.titleLabelFont = UIFontMake(16); + dialogViewControllerAppearance.titleLabelTextColor = UIColorMake(53, 60, 70); + dialogViewControllerAppearance.subTitleLabelFont = UIFontMake(12); + dialogViewControllerAppearance.subTitleLabelTextColor = UIColorMake(133, 140, 150); + + dialogViewControllerAppearance.headerSeparatorColor = UIColorMake(222, 224, 226); + dialogViewControllerAppearance.headerViewHeight = 48; + dialogViewControllerAppearance.headerViewBackgroundColor = UIColorMake(244, 245, 247); + dialogViewControllerAppearance.contentViewMargins = UIEdgeInsetsZero; + dialogViewControllerAppearance.contentViewBackgroundColor = nil; + dialogViewControllerAppearance.footerSeparatorColor = UIColorMake(222, 224, 226); + dialogViewControllerAppearance.footerViewHeight = 48; + dialogViewControllerAppearance.footerViewBackgroundColor = nil; + + dialogViewControllerAppearance.buttonBackgroundColor = nil; + dialogViewControllerAppearance.buttonTitleAttributes = @{NSForegroundColorAttributeName: UIColorBlue}; + dialogViewControllerAppearance.buttonHighlightedBackgroundColor = [UIColorBlue colorWithAlphaComponent:.25]; }); - return dialogViewControllerAppearance; } @end @@ -80,28 +74,8 @@ @implementation QMUIDialogViewController - (void)didInitialize { [super didInitialize]; - if (dialogViewControllerAppearance) { - self.cornerRadius = [QMUIDialogViewController appearance].cornerRadius; - self.dialogViewMargins = UIEdgeInsetsConcat([QMUIDialogViewController appearance].dialogViewMargins, SafeAreaInsetsConstantForDeviceWithNotch); - self.maximumContentViewWidth = [QMUIDialogViewController appearance].maximumContentViewWidth; - self.backgroundColor = [QMUIDialogViewController appearance].backgroundColor; - self.titleTintColor = [QMUIDialogViewController appearance].titleTintColor; - self.titleLabelFont = [QMUIDialogViewController appearance].titleLabelFont; - self.titleLabelTextColor = [QMUIDialogViewController appearance].titleLabelTextColor; - self.subTitleLabelFont = [QMUIDialogViewController appearance].subTitleLabelFont; - self.subTitleLabelTextColor = [QMUIDialogViewController appearance].subTitleLabelTextColor; - self.headerSeparatorColor = [QMUIDialogViewController appearance].headerSeparatorColor; - self.headerViewHeight = [QMUIDialogViewController appearance].headerViewHeight; - self.headerViewBackgroundColor = [QMUIDialogViewController appearance].headerViewBackgroundColor; - self.contentViewMargins = [QMUIDialogViewController appearance].contentViewMargins; - self.contentViewBackgroundColor = [QMUIDialogViewController appearance].contentViewBackgroundColor; - self.footerSeparatorColor = [QMUIDialogViewController appearance].footerSeparatorColor; - self.footerViewHeight = [QMUIDialogViewController appearance].footerViewHeight; - self.footerViewBackgroundColor = [QMUIDialogViewController appearance].footerViewBackgroundColor; - self.buttonBackgroundColor = [QMUIDialogViewController appearance].buttonBackgroundColor; - self.buttonTitleAttributes = [QMUIDialogViewController appearance].buttonTitleAttributes; - self.buttonHighlightedBackgroundColor = [QMUIDialogViewController appearance].buttonHighlightedBackgroundColor; - } + + [self qmui_applyAppearance]; _contentView = [[UIView alloc] init]; // 特地不使用setter,从而不要影响self.hasCustomContentView的默认值 self.contentView.backgroundColor = self.contentViewBackgroundColor; @@ -153,7 +127,7 @@ - (void)setBackgroundColor:(UIColor *)backgroundColor { - (void)setTitleTintColor:(UIColor *)titleTintColor { _titleTintColor = titleTintColor; - self.titleView.tintColor = titleTintColor; + [self updateTitleViewColor]; } - (void)setTitleLabelFont:(UIFont *)titleLabelFont { @@ -164,7 +138,7 @@ - (void)setTitleLabelFont:(UIFont *)titleLabelFont { - (void)setTitleLabelTextColor:(UIColor *)titleLabelTextColor { _titleLabelTextColor = titleLabelTextColor; - self.titleView.titleLabel.textColor = titleLabelTextColor; + [self updateTitleViewColor]; } - (void)setSubTitleLabelFont:(UIFont *)subTitleLabelFont { @@ -175,7 +149,17 @@ - (void)setSubTitleLabelFont:(UIFont *)subTitleLabelFont { - (void)setSubTitleLabelTextColor:(UIColor *)subTitleLabelTextColor { _subTitleLabelTextColor = subTitleLabelTextColor; - self.titleView.subtitleLabel.textColor = subTitleLabelTextColor; + [self updateTitleViewColor]; +} + +- (void)updateTitleViewColor { + self.titleView.adjustsSubviewsTintColorAutomatically = !self.titleLabelTextColor && !self.subTitleLabelTextColor; + if (self.titleView.adjustsSubviewsTintColorAutomatically) { + self.titleView.tintColor = self.titleTintColor;// call tintColorDidChange + } else { + self.titleView.titleLabel.textColor = self.titleLabelTextColor ?: (self.titleTintColor ?: self.titleView.tintColor); + self.titleView.subtitleLabel.textColor = self.subTitleLabelTextColor ?: (self.titleTintColor ?: self.titleView.tintColor); + } } - (void)setHeaderSeparatorColor:(UIColor *)headerSeparatorColor { @@ -364,7 +348,6 @@ - (void)removeSubmitButton { - (QMUIButton *)generateButtonWithText:(NSString *)buttonText { QMUIButton *button = [[QMUIButton alloc] init]; button.titleLabel.font = UIFontBoldMake((IS_320WIDTH_SCREEN) ? 14 : 15); - button.adjustsTitleTintColorAutomatically = YES; button.backgroundColor = self.buttonBackgroundColor; button.highlightedBackgroundColor = self.buttonHighlightedBackgroundColor; [button setAttributedTitle:[[NSAttributedString alloc] initWithString:buttonText attributes:self.buttonTitleAttributes] forState:UIControlStateNormal]; @@ -438,23 +421,15 @@ - (void)hideModalPresentationComponent { @implementation QMUIDialogSelectionViewController (UIAppearance) -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self appearance]; - }); ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; } -static QMUIDialogSelectionViewController *dialogSelectionViewControllerAppearance; -+ (nonnull instancetype)appearance { ++ (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - if (!dialogSelectionViewControllerAppearance) { - dialogSelectionViewControllerAppearance = [[QMUIDialogSelectionViewController alloc] init]; - dialogSelectionViewControllerAppearance.rowHeight = TableViewCellNormalHeight; - } + QMUIDialogSelectionViewController.appearance.rowHeight = TableViewCellNormalHeight; }); - return dialogSelectionViewControllerAppearance; } @end @@ -471,22 +446,14 @@ @implementation QMUIDialogSelectionViewController - (void)didInitialize { [super didInitialize]; - if (dialogSelectionViewControllerAppearance) { - self.rowHeight = [QMUIDialogSelectionViewController appearance].rowHeight; - } - self.selectedItemIndex = QMUIDialogSelectionViewControllerSelectedItemIndexNone; self.selectedItemIndexes = [[NSMutableSet alloc] init]; self.tableView = [[QMUITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; - self.tableView.delegate = self; self.tableView.dataSource = self; + self.tableView.delegate = self; self.tableView.alwaysBounceVertical = NO; - if (@available(iOS 11, *)) { - self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } else { - self.automaticallyAdjustsScrollViewInsets = NO; - } + self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; // 因为要根据 tableView sizeThatFits: 算出 dialog 的高度,所以禁用 estimated 特性,不然算出来结果不准确 self.tableView.estimatedRowHeight = 0; @@ -646,31 +613,24 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath @implementation QMUIDialogTextFieldViewController (UIAppearance) -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self appearance]; - }); ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; } -static QMUIDialogTextFieldViewController *dialogTextFieldViewControllerAppearance; -+ (nonnull instancetype)appearance { ++ (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - if (!dialogTextFieldViewControllerAppearance) { - dialogTextFieldViewControllerAppearance = [[QMUIDialogTextFieldViewController alloc] init]; - dialogTextFieldViewControllerAppearance.textFieldLabelFont = UIFontBoldMake(12); - dialogTextFieldViewControllerAppearance.textFieldLabelTextColor = UIColorGrayDarken; - dialogTextFieldViewControllerAppearance.textFieldFont = UIFontMake(17); - dialogTextFieldViewControllerAppearance.textFieldTextColor = UIColorBlack; - dialogTextFieldViewControllerAppearance.textFieldSeparatorColor = UIColorSeparator; - dialogTextFieldViewControllerAppearance.textFieldLabelMargins = UIEdgeInsetsMake(16, 22, -2, 22); - dialogTextFieldViewControllerAppearance.textFieldMargins = UIEdgeInsetsMake(16, 16, 10, 16); - dialogTextFieldViewControllerAppearance.textFieldHeight = 25; - dialogTextFieldViewControllerAppearance.textFieldSeparatorInsets = UIEdgeInsetsMake(0, 0, 16, 0); - } + QMUIDialogTextFieldViewController *dialogTextFieldViewControllerAppearance = QMUIDialogTextFieldViewController.appearance; + dialogTextFieldViewControllerAppearance.textFieldLabelFont = UIFontBoldMake(12); + dialogTextFieldViewControllerAppearance.textFieldLabelTextColor = UIColorGrayDarken; + dialogTextFieldViewControllerAppearance.textFieldFont = UIFontMake(17); + dialogTextFieldViewControllerAppearance.textFieldTextColor = UIColorBlack; + dialogTextFieldViewControllerAppearance.textFieldSeparatorColor = UIColorSeparator; + dialogTextFieldViewControllerAppearance.textFieldLabelMargins = UIEdgeInsetsMake(16, 22, -2, 22); + dialogTextFieldViewControllerAppearance.textFieldMargins = UIEdgeInsetsMake(16, 16, 10, 16); + dialogTextFieldViewControllerAppearance.textFieldHeight = 25; + dialogTextFieldViewControllerAppearance.textFieldSeparatorInsets = UIEdgeInsetsMake(0, 0, 16, 0); }); - return dialogTextFieldViewControllerAppearance; } @end @@ -688,18 +648,6 @@ @implementation QMUIDialogTextFieldViewController - (void)didInitialize { [super didInitialize]; - if (dialogTextFieldViewControllerAppearance) { - self.textFieldLabelFont = [QMUIDialogTextFieldViewController appearance].textFieldLabelFont; - self.textFieldLabelTextColor = [QMUIDialogTextFieldViewController appearance].textFieldLabelTextColor; - self.textFieldFont = [QMUIDialogTextFieldViewController appearance].textFieldFont; - self.textFieldTextColor = [QMUIDialogTextFieldViewController appearance].textFieldTextColor; - self.textFieldSeparatorColor = [QMUIDialogTextFieldViewController appearance].textFieldSeparatorColor; - self.textFieldLabelMargins = [QMUIDialogTextFieldViewController appearance].textFieldLabelMargins; - self.textFieldMargins = [QMUIDialogTextFieldViewController appearance].textFieldMargins; - self.textFieldHeight = [QMUIDialogTextFieldViewController appearance].textFieldHeight; - self.textFieldSeparatorInsets = [QMUIDialogTextFieldViewController appearance].textFieldSeparatorInsets; - } - self.mutableTitleLabels = [[NSMutableArray alloc] init]; self.mutableTextFields = [[NSMutableArray alloc] init]; self.mutableSeparatorLayers = [[NSMutableArray alloc] init]; @@ -710,11 +658,7 @@ - (void)didInitialize { self.scrollView = [[UIScrollView alloc] init]; self.scrollView.scrollsToTop = NO; self.scrollView.clipsToBounds = YES; - if (@available(iOS 11, *)) { - self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } else { - self.automaticallyAdjustsScrollViewInsets = NO; - } + self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; self.contentView = self.scrollView; self.scrollView.backgroundColor = self.contentViewBackgroundColor; } @@ -771,7 +715,7 @@ - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; // 全部基于 contentView 布局即可 - NSAssert(self.mutableTitleLabels.count == self.mutableTextFields.count && self.mutableTextFields.count == self.mutableSeparatorLayers.count, @"%@ 里的标题、输入框、分隔线数量不匹配", NSStringFromClass(self.class)); + QMUIAssert(self.mutableTitleLabels.count == self.mutableTextFields.count && self.mutableTextFields.count == self.mutableSeparatorLayers.count, NSStringFromClass(self.class), @"标题、输入框、分隔线的数量不匹配"); CGFloat minY = 0; @@ -977,7 +921,7 @@ - (CGSize)preferredContentSizeInModalPresentationViewController:(QMUIModalPresen } } - CGFloat contentHeight = textFieldLabelHeight + textFieldHeight + separatorHeight + UIEdgeInsetsGetVerticalValue(self.scrollView.qmui_contentInset); + CGFloat contentHeight = textFieldLabelHeight + textFieldHeight + separatorHeight + UIEdgeInsetsGetVerticalValue(self.scrollView.adjustedContentInset); CGFloat contentViewVerticalMargin = UIEdgeInsetsGetVerticalValue(self.contentViewMargins); BOOL isFooterViewShowing = self.footerView && !self.footerView.hidden; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.h b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.h index 87005d3c..f9e3b0ae 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIEmotionInputManager.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.m b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.m index 323d5a96..b594a698 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIEmotionInputManager.m diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.h b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.h index d87cb681..7bf3e296 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIEmotionView.h @@ -47,7 +47,7 @@ * 表情控件,支持任意表情的展示,每个表情以相同的大小显示。 * * 使用方式: - * + * * - 通过`initWithFrame:`初始化,如果面板高度不变,建议在init时就设置好,若最终布局以父类的`layoutSubviews`为准,则也可通过`init`方法初始化,再在`layoutSubviews`里计算布局 * - 通过调整`paddingInPage`、`emotionSize`等变量来自定义UI * - 通过`emotions`设置要展示的表情 @@ -75,6 +75,15 @@ /// 用于展示表情面板的横向滚动collectionView,布局撑满整个控件 @property(nonatomic, strong, readonly) UICollectionView *collectionView; +/// 用于展示表情面板的竖向滚动的 scrollView,布局撑满整个控件 +@property(nonatomic, strong, readonly) UIScrollView *scrollView; + +/// 竖向滚动,默认为 NO +@property(nonatomic, assign) BOOL verticalAlignment UI_APPEARANCE_SELECTOR; + +/// 表情与表情之间的垂直间距,默认为10,仅在 verticalAlignment 为 YES 时生效,当 verticalAlignment 为 N0 时,表情的垂直间距由 numberOfRowsPerPage 决定 +@property(nonatomic, assign) CGFloat emotionVerticalSpacing UI_APPEARANCE_SELECTOR; + /// 用于横向按页滚动的collectionViewLayout @property(nonatomic, strong, readonly) UICollectionViewFlowLayout *collectionViewLayout; @@ -84,6 +93,9 @@ /// 控件右下角的发送按钮 @property(nonatomic, strong, readonly) QMUIButton *sendButton; +/// 控件右下角的删除按钮 +@property(nonatomic, strong, readonly) QMUIButton *deleteButton; + /// 每一页表情的上下左右padding,默认为{18, 18, 65, 18} @property(nonatomic, assign) UIEdgeInsets paddingInPage UI_APPEARANCE_SELECTOR; @@ -102,6 +114,15 @@ /// 表情面板右下角的删除按钮的图片,默认为`[QMUIHelper imageWithName:@"QMUI_emotion_delete"]` @property(nonatomic, strong) UIImage *deleteButtonImage UI_APPEARANCE_SELECTOR; +/// 删除按钮的背景色,默认为 nil +@property(nonatomic, strong) UIColor *deleteButtonBackgroundColor UI_APPEARANCE_SELECTOR; + +/// 删除按钮位置的 (x,y) 的偏移,默认为 CGPointZero +@property(nonatomic, assign) CGPoint deleteButtonOffset UI_APPEARANCE_SELECTOR; + +/// 删除按钮的圆角大小,默认为4 +@property(nonatomic, assign) CGFloat deleteButtonCornerRadius UI_APPEARANCE_SELECTOR; + /// 发送按钮的文字样式,默认为{NSFontAttributeName: UIFontMake(15), NSForegroundColorAttributeName: UIColorWhite} @property(nonatomic, strong) NSDictionary *sendButtonTitleAttributes UI_APPEARANCE_SELECTOR; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.m b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.m index 932dbc86..e73bfd7b 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIEmotionView.m @@ -17,6 +17,7 @@ #import "QMUICore.h" #import "QMUIButton.h" #import "UIView+QMUI.h" +#import "CALayer+QMUI.h" #import "UIScrollView+QMUI.h" #import "UIControl+QMUI.h" #import "UIImage+QMUI.h" @@ -25,12 +26,19 @@ @implementation QMUIEmotion + (instancetype)emotionWithIdentifier:(NSString *)identifier displayName:(NSString *)displayName { - QMUIEmotion *emotion = [[QMUIEmotion alloc] init]; + QMUIEmotion *emotion = [[self alloc] init]; emotion.identifier = identifier; emotion.displayName = displayName; return emotion; } +- (BOOL)isEqual:(id)object { + if (!object) return NO; + if (self == object) return YES; + if (![object isKindOfClass:[self class]]) return NO; + return [self.identifier isEqualToString:((QMUIEmotion *)object).identifier]; +} + - (NSString *)description { return [NSString stringWithFormat:@"%@, identifier: %@, displayName: %@", [super description], self.identifier, self.displayName]; } @@ -43,8 +51,7 @@ @protocol QMUIEmotionPageViewDelegate @optional - (void)emotionPageView:(QMUIEmotionPageView *)emotionPageView didSelectEmotion:(QMUIEmotion *)emotion atIndex:(NSInteger)index; -- (void)didSelectDeleteButtonInEmotionPageView:(QMUIEmotionPageView *)emotionPageView; - +- (void)emotionPageViewDidLayoutEmotions:(QMUIEmotionPageView *)emotionPageView; @end /// 表情面板每一页的cell,在drawRect里将所有表情绘制上去,同时自带一个末尾的删除按钮 @@ -56,7 +63,16 @@ @interface QMUIEmotionPageView : UICollectionViewCell @property(nonatomic, strong) UIView *emotionSelectedBackgroundView; /// 表情面板右下角的删除按钮 -@property(nonatomic, strong) QMUIButton *deleteButton; +@property(nonatomic, weak) QMUIButton *deleteButton; + +/// 表情面板右下角的删除按的截图,因为在 CollectionView 滑动的过程中可能会出现 2 个 deleteButton,但是真实的 deleteButton 只能有一个,所以用截图来过渡 +@property(nonatomic, strong) UIView *deleteButtonSnapView; + +/// 删除按钮位置的 (x,y) 的偏移 +@property(nonatomic, assign) CGPoint deleteButtonOffset; + +/// 所有表情的 Layer +@property(nonatomic, strong) NSMutableArray *emotionLayers; /// 分配给当前pageView的所有表情 @property(nonatomic, copy) NSArray *emotions; @@ -84,6 +100,11 @@ @interface QMUIEmotionPageView : UICollectionViewCell /// debug模式会把表情的绘制矩形显示出来 @property(nonatomic, assign) BOOL debug; + +@property(nonatomic, assign, readonly) BOOL needsLayoutEmotions; + +@property(nonatomic, assign) CGRect previousLayoutFrame; + @end @implementation QMUIEmotionPageView @@ -100,12 +121,6 @@ - (instancetype)initWithFrame:(CGRect)frame { self.emotionSelectedBackgroundView.alpha = 0; [self addSubview:self.emotionSelectedBackgroundView]; - self.deleteButton = [[QMUIButton alloc] init]; - self.deleteButton.adjustsButtonWhenHighlighted = NO;// 去掉QMUIButton默认的高亮动画,从而加快连续快速点击的响应速度 - self.deleteButton.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; - [self.deleteButton addTarget:self action:@selector(handleDeleteButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self addSubview:self.deleteButton]; - self.emotionHittingRects = [[NSMutableArray alloc] init]; self.tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGestureRecognizer:)]; [self addGestureRecognizer:self.tapGestureRecognizer]; @@ -113,58 +128,101 @@ - (instancetype)initWithFrame:(CGRect)frame { return self; } +- (CGRect)frameForDeleteButton:(__kindof UIView *)deleteButton { + return CGRectSetXY(deleteButton.frame, CGRectGetWidth(self.bounds) - self.padding.right - CGRectGetWidth(deleteButton.frame) - (self.emotionSize.width - CGRectGetWidth(deleteButton.frame)) / 2.0 + self.deleteButtonOffset.x, CGRectGetHeight(self.bounds) - self.padding.bottom - CGRectGetHeight(deleteButton.frame) - (self.emotionSize.height - CGRectGetHeight(deleteButton.frame)) / 2.0 + self.deleteButtonOffset.y); +} + - (void)layoutSubviews { [super layoutSubviews]; - // 删除按钮必定布局到最后一个表情的位置,且与表情上下左右居中 - [self.deleteButton sizeToFit]; - self.deleteButton.frame = CGRectSetXY(self.deleteButton.frame, CGRectGetWidth(self.bounds) - self.padding.right - CGRectGetWidth(self.deleteButton.frame) - (self.emotionSize.width - CGRectGetWidth(self.deleteButton.frame)) / 2.0, CGRectGetHeight(self.bounds) - self.padding.bottom - CGRectGetHeight(self.deleteButton.frame) - (self.emotionSize.height - CGRectGetHeight(self.deleteButton.frame)) / 2.0); + + if (self.deleteButton.superview == self) { + // 删除按钮必定布局到最后一个表情的位置,且与表情上下左右居中 + [self.deleteButton sizeToFit]; + self.deleteButton.frame = [self frameForDeleteButton:self.deleteButton]; + } + if (self.deleteButtonSnapView) { + self.deleteButtonSnapView.frame = [self frameForDeleteButton:self.deleteButtonSnapView]; + } + BOOL isSizeChanged = !CGSizeEqualToSize(self.previousLayoutFrame.size, self.frame.size); + self.previousLayoutFrame = self.frame; + if (isSizeChanged) { + [self setNeedsLayoutEmotions]; + } + [self layoutEmotionsIfNeeded]; +} + +- (void)willRemoveSubview:(UIView *)subview { + if (subview == self.deleteButton) { + self.deleteButtonSnapView = [self.deleteButton snapshotViewAfterScreenUpdates:NO]; + [self addSubview:self.deleteButtonSnapView]; + } +} + +- (void)setNeedsLayoutEmotions { + _needsLayoutEmotions = YES; +} + +- (void)setEmotions:(NSArray *)emotions { + if ([_emotions isEqualToArray:emotions]) return; + _emotions = emotions; + [self setNeedsLayoutEmotions]; + [self setNeedsLayout]; } -- (void)drawRect:(CGRect)rect { +- (void)layoutEmotionsIfNeeded { + if (!self.needsLayoutEmotions) return; + _needsLayoutEmotions = NO; [self.emotionHittingRects removeAllObjects]; CGSize contentSize = CGRectInsetEdges(self.bounds, self.padding).size; NSInteger emotionCountPerRow = (contentSize.width + self.minimumEmotionHorizontalSpacing) / (self.emotionSize.width + self.minimumEmotionHorizontalSpacing); CGFloat emotionHorizontalSpacing = flat((contentSize.width - emotionCountPerRow * self.emotionSize.width) / (emotionCountPerRow - 1)); CGFloat emotionVerticalSpacing = flat((contentSize.height - self.numberOfRows * self.emotionSize.height) / (self.numberOfRows - 1)); - CGPoint emotionOrigin = CGPointZero; - for (NSInteger i = 0, l = self.emotions.count; i < l; i++) { + NSInteger emotionCount = self.emotions.count; + if (!self.emotionLayers) { + self.emotionLayers = [NSMutableArray arrayWithCapacity:emotionCount]; + } + for (NSInteger i = 0; i < emotionCount; i++) { + CALayer *emotionlayer = nil; + if (i < self.emotionLayers.count) { + emotionlayer = self.emotionLayers[i]; + } else { + emotionlayer = [CALayer layer]; + emotionlayer.contentsScale = ScreenScale; + [self.emotionLayers addObject:emotionlayer]; + [self.layer addSublayer:emotionlayer]; + } + + emotionlayer.contents = (__bridge id)(self.emotions[i].image.CGImage); NSInteger row = i / emotionCountPerRow; emotionOrigin.x = self.padding.left + (self.emotionSize.width + emotionHorizontalSpacing) * (i % emotionCountPerRow); emotionOrigin.y = self.padding.top + (self.emotionSize.height + emotionVerticalSpacing) * row; - QMUIEmotion *emotion = self.emotions[i]; CGRect emotionRect = CGRectMake(emotionOrigin.x, emotionOrigin.y, self.emotionSize.width, self.emotionSize.height); CGRect emotionHittingRect = CGRectInsetEdges(emotionRect, self.emotionSelectedBackgroundExtension); [self.emotionHittingRects addObject:[NSValue valueWithCGRect:emotionHittingRect]]; - [self drawImage:emotion.image inRect:emotionRect]; + emotionlayer.frame = emotionRect; + emotionlayer.hidden = NO; } -} - -- (void)drawImage:(UIImage *)image inRect:(CGRect)contextRect { - CGSize imageSize = image.size; - CGFloat horizontalRatio = CGRectGetWidth(contextRect) / imageSize.width; - CGFloat verticalRatio = CGRectGetHeight(contextRect) / imageSize.height; - // 表情图片按UIViewContentModeScaleAspectFit的方式来绘制 - CGFloat ratio = fmin(horizontalRatio, verticalRatio); - CGRect drawingRect = CGRectZero; - drawingRect.size.width = imageSize.width * ratio; - drawingRect.size.height = imageSize.height * ratio; - drawingRect = CGRectSetXY(drawingRect, CGRectGetMinXHorizontallyCenter(contextRect, drawingRect), CGRectGetMinYVerticallyCenter(contextRect, drawingRect)); - if (self.debug) { - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextSetLineWidth(context, PixelOne); - CGContextSetStrokeColorWithColor(context, UIColorTestRed.CGColor); - CGContextStrokeRect(context, CGRectInset(contextRect, PixelOne / 2.0, PixelOne / 2.0)); + + if (self.emotionLayers.count > emotionCount) { + for (NSInteger i = self.emotionLayers.count - emotionCount - 1; i < self.emotionLayers.count; i++) { + self.emotionLayers[i].hidden = YES; + } + } + if ([self.delegate respondsToSelector:@selector(emotionPageViewDidLayoutEmotions:)]) { + [self.delegate emotionPageViewDidLayoutEmotions:self]; } - [image drawInRect:drawingRect]; } + - (void)handleTapGestureRecognizer:(UITapGestureRecognizer *)gestureRecognizer { CGPoint location = [gestureRecognizer locationInView:self]; for (NSInteger i = 0; i < self.emotionHittingRects.count; i ++) { CGRect rect = [self.emotionHittingRects[i] CGRectValue]; if (CGRectContainsPoint(rect, location)) { + CALayer *layer = self.emotionLayers[i]; + if (layer.opacity < 0.2) return; QMUIEmotion *emotion = self.emotions[i]; self.emotionSelectedBackgroundView.frame = rect; [UIView animateWithDuration:.08 animations:^{ @@ -185,16 +243,98 @@ - (void)handleTapGestureRecognizer:(UITapGestureRecognizer *)gestureRecognizer { } } -- (void)handleDeleteButtonEvent:(QMUIButton *)deleteButton { - if ([self.delegate respondsToSelector:@selector(didSelectDeleteButtonInEmotionPageView:)]) { - [self.delegate didSelectDeleteButtonInEmotionPageView:self]; +- (CGSize)verticalSizeThatFits:(CGSize)size emotionVerticalSpacing:(CGFloat)emotionVerticalSpacing { + CGSize contentSize = CGRectInsetEdges(CGRectMakeWithSize(size), self.padding).size; + NSInteger emotionCountPerRow = (contentSize.width + self.minimumEmotionHorizontalSpacing) / (self.emotionSize.width + self.minimumEmotionHorizontalSpacing); + NSInteger row = ceil(self.emotions.count / (emotionCountPerRow * 1.0)); + CGFloat height = (self.emotionSize.height + emotionVerticalSpacing) * row - emotionVerticalSpacing + UIEdgeInsetsGetVerticalValue(self.padding); + return CGSizeMake(size.width, height); +} + +- (void)updateDeleteButton:(QMUIButton *)deleteButton { + _deleteButton = deleteButton; + if (self.deleteButtonSnapView) { + [self.deleteButtonSnapView removeFromSuperview]; + self.deleteButtonSnapView = nil; } + [self addSubview:deleteButton]; +} + +- (void)setDeleteButtonOffset:(CGPoint)deleteButtonOffset { + _deleteButtonOffset = deleteButtonOffset; + [self setNeedsLayout]; } + @end -@interface QMUIEmotionView () +@interface QMUIEmotionVerticalScrollView : UIScrollView +@property(nonatomic, strong) QMUIEmotionPageView *pageView; +@end +@implementation QMUIEmotionVerticalScrollView + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + _pageView = [[QMUIEmotionPageView alloc] init]; + self.pageView.deleteButton.hidden = YES; + [self addSubview:self.pageView]; + } + return self; +} + +- (void)setEmotions:(NSArray *)emotions + emotionSize:(CGSize)emotionSize + minimumEmotionHorizontalSpacing:(CGFloat)minimumEmotionHorizontalSpacing + emotionVerticalSpacing:(CGFloat)emotionVerticalSpacing + emotionSelectedBackgroundExtension:(UIEdgeInsets)emotionSelectedBackgroundExtension + paddingInPage:(UIEdgeInsets)paddingInPage { + QMUIEmotionPageView *pageView = self.pageView; + pageView.emotions = emotions; + pageView.padding = paddingInPage; + CGSize contentSize = CGSizeMake(self.bounds.size.width - UIEdgeInsetsGetHorizontalValue(paddingInPage), self.bounds.size.height - UIEdgeInsetsGetVerticalValue(paddingInPage)); + NSInteger emotionCountPerRow = (contentSize.width + minimumEmotionHorizontalSpacing) / (emotionSize.width + minimumEmotionHorizontalSpacing); + pageView.numberOfRows = ceil(emotions.count / (CGFloat)emotionCountPerRow); + pageView.emotionSize =emotionSize; + pageView.emotionSelectedBackgroundExtension = emotionSelectedBackgroundExtension; + pageView.minimumEmotionHorizontalSpacing = minimumEmotionHorizontalSpacing; + [pageView setNeedsLayout]; + CGSize size = [pageView verticalSizeThatFits:self.bounds.size emotionVerticalSpacing:emotionVerticalSpacing]; + self.pageView.frame = CGRectMakeWithSize(size); + self.contentSize = size; +} + +- (void)adjustEmotionsAlphaWithFloatingRect:(CGRect)floatingRect { + CGSize contentSize = CGSizeMake(self.contentSize.width - UIEdgeInsetsGetHorizontalValue(self.pageView.padding), self.contentSize.height - UIEdgeInsetsGetVerticalValue(self.pageView.padding)); + NSInteger emotionCountPerRow = (contentSize.width + self.pageView.minimumEmotionHorizontalSpacing) / (self.pageView.emotionSize.width + self.pageView.minimumEmotionHorizontalSpacing); + CGFloat emotionVerticalSpacing = flat((contentSize.height - self.pageView.numberOfRows * self.pageView.emotionSize.height) / (self.pageView.numberOfRows - 1)); + NSInteger columnIndexLeft = ceil((floatingRect.origin.x - self.pageView.padding.left) / (self.pageView.emotionSize.width + self.pageView.minimumEmotionHorizontalSpacing)) - 1; + NSInteger columnIndexRight = emotionCountPerRow - 1; + CGFloat rowIndexTop = ((floatingRect.origin.y - self.pageView.padding.top) / (self.pageView.emotionSize.height + emotionVerticalSpacing)) - 1; + for (NSInteger i = 0; i < self.pageView.emotionLayers.count; i++) { + NSInteger row = (i / emotionCountPerRow); + NSInteger column = (i % emotionCountPerRow); + [CALayer qmui_performWithoutAnimation:^{ + if (column >= columnIndexLeft && column <= columnIndexRight && row > rowIndexTop) { + if (row == ceil(rowIndexTop)) { + CGFloat intersectAreaHeight = floatingRect.origin.y - self.pageView.emotionLayers[i].frame.origin.y; + CGFloat percent = intersectAreaHeight / self.pageView.emotionSize.height; + self.pageView.emotionLayers[i].opacity = percent * percent; + } else { + self.pageView.emotionLayers[i].opacity = 0; + } + } else { + self.pageView.emotionLayers[i].opacity = 1.0f; + } + }]; + } +} + +@end + +@interface QMUIEmotionView () +/// 用于展示表情面板的竖向滚动 scrollView,布局撑满整个控件 +@property(nonatomic, strong, readonly) QMUIEmotionVerticalScrollView *verticalScrollView; @property(nonatomic, strong) NSMutableArray *> *pagedEmotions; @property(nonatomic, assign) BOOL debug; @end @@ -216,6 +356,17 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { return self; } +- (void)setVerticalAlignment:(BOOL)verticalAlignment { + _verticalAlignment = verticalAlignment; + self.collectionView.hidden = verticalAlignment; + self.pageControl.hidden = verticalAlignment; + self.verticalScrollView.hidden = !verticalAlignment; + if (!verticalAlignment && self.deleteButton.superview) { + [self.deleteButton removeFromSuperview]; + } + [self setNeedsLayout]; +} + - (void)didInitializedWithFrame:(CGRect)frame { self.debug = NO; @@ -227,7 +378,8 @@ - (void)didInitializedWithFrame:(CGRect)frame { self.collectionViewLayout.minimumInteritemSpacing = 0; self.collectionViewLayout.sectionInset = UIEdgeInsetsZero; - _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(self.qmui_safeAreaInsets.left, self.qmui_safeAreaInsets.top, CGRectGetWidth(frame) - UIEdgeInsetsGetHorizontalValue(self.qmui_safeAreaInsets), CGRectGetHeight(frame) - UIEdgeInsetsGetVerticalValue(self.qmui_safeAreaInsets)) collectionViewLayout:self.collectionViewLayout]; + _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(self.safeAreaInsets.left, self.safeAreaInsets.top, CGRectGetWidth(frame) - UIEdgeInsetsGetHorizontalValue(self.safeAreaInsets), CGRectGetHeight(frame) - UIEdgeInsetsGetVerticalValue(self.safeAreaInsets)) collectionViewLayout:self.collectionViewLayout]; + self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; self.collectionView.backgroundColor = UIColorClear; self.collectionView.scrollsToTop = NO; self.collectionView.pagingEnabled = YES; @@ -237,6 +389,12 @@ - (void)didInitializedWithFrame:(CGRect)frame { [self.collectionView registerClass:[QMUIEmotionPageView class] forCellWithReuseIdentifier:@"page"]; [self addSubview:self.collectionView]; + _verticalScrollView = [[QMUIEmotionVerticalScrollView alloc] init]; + self.verticalScrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + _verticalScrollView.delegate = self; + _verticalScrollView.hidden = YES; + [self addSubview:self.verticalScrollView]; + _pageControl = [[UIPageControl alloc] init]; [self.pageControl addTarget:self action:@selector(handlePageControlEvent:) forControlEvents:UIControlEventValueChanged]; [self addSubview:self.pageControl]; @@ -244,33 +402,66 @@ - (void)didInitializedWithFrame:(CGRect)frame { _sendButton = [[QMUIButton alloc] init]; [self.sendButton setTitle:@"发送" forState:UIControlStateNormal]; self.sendButton.contentEdgeInsets = UIEdgeInsetsMake(5, 17, 5, 17); - [self.sendButton sizeToFit]; [self addSubview:self.sendButton]; + + _deleteButton = [[QMUIButton alloc] init]; + self.deleteButton.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; + __weak __typeof(self)weakSelf = self; + self.deleteButton.qmui_tapBlock = ^(__kindof UIControl *sender) { + __strong __typeof(weakSelf)strongSelf = weakSelf; + if (strongSelf.didSelectDeleteButtonBlock) { + strongSelf.didSelectDeleteButtonBlock(); + } + }; } - (void)setEmotions:(NSArray *)emotions { _emotions = emotions; - [self pageEmotions]; + if (self.verticalAlignment) { + [self setNeedsLayout]; + } else { + [self pageEmotions]; + } } - (void)layoutSubviews { [super layoutSubviews]; - CGRect collectionViewFrame = CGRectInsetEdges(self.bounds, self.qmui_safeAreaInsets); - BOOL collectionViewSizeChanged = !CGSizeEqualToSize(collectionViewFrame.size, self.collectionView.bounds.size); - self.collectionViewLayout.itemSize = collectionViewFrame.size;// 先更新 itemSize 再设置 collectionView.frame,否则会触发系统的 UICollectionViewFlowLayoutBreakForInvalidSizes 断点 - self.collectionView.frame = collectionViewFrame; - if (collectionViewSizeChanged) { - [self pageEmotions]; + [self.sendButton sizeToFit]; + self.sendButton.qmui_right = self.qmui_width - self.safeAreaInsets.right - self.sendButtonMargins.right; + self.sendButton.qmui_bottom = self.qmui_height - self.safeAreaInsets.bottom - self.sendButtonMargins.bottom; + if (self.verticalAlignment) { + CGRect verticalScrollViewFrame = CGRectInsetEdges(self.bounds, UIEdgeInsetsSetBottom(self.safeAreaInsets, 0)); + self.verticalScrollView.frame = verticalScrollViewFrame; + [self.verticalScrollView setEmotions:self.emotions + emotionSize:self.emotionSize + minimumEmotionHorizontalSpacing:self.minimumEmotionHorizontalSpacing + emotionVerticalSpacing:self.emotionVerticalSpacing + emotionSelectedBackgroundExtension:self.emotionSelectedBackgroundExtension + paddingInPage:UIEdgeInsetsSetBottom(self.paddingInPage, self.paddingInPage.bottom + self.safeAreaInsets.bottom)]; + self.verticalScrollView.pageView.delegate = self; + [self addSubview:self.deleteButton]; + [self.deleteButton setImage:self.deleteButtonImage forState:UIControlStateNormal]; + [self.deleteButton setImage:[self.deleteButtonImage qmui_imageWithAlpha:ButtonHighlightedAlpha] forState:UIControlStateHighlighted]; + self.deleteButton.bounds = CGRectMakeWithSize(CGSizeMake([self.deleteButton sizeThatFits:CGSizeZero].width, self.sendButton.qmui_height)); + static CGFloat spacingBetweenDeleteButtonAndSendButton = 4.0f; + self.deleteButton.qmui_right = self.sendButton.qmui_left - spacingBetweenDeleteButtonAndSendButton + self.deleteButtonOffset.x; + self.deleteButton.qmui_top = CGRectGetMinYVerticallyCenter(self.sendButton.frame, self.deleteButton.frame) + self.deleteButtonOffset.y; + + } else { + CGRect collectionViewFrame = CGRectInsetEdges(self.bounds, self.safeAreaInsets); + BOOL collectionViewSizeChanged = !CGSizeEqualToSize(collectionViewFrame.size, self.collectionView.bounds.size); + self.collectionViewLayout.itemSize = collectionViewFrame.size;// 先更新 itemSize 再设置 collectionView.frame,否则会触发系统的 UICollectionViewFlowLayoutBreakForInvalidSizes 断点 + self.collectionView.frame = collectionViewFrame; + + if (collectionViewSizeChanged) { + [self pageEmotions]; + } + CGFloat pageControlHeight = 16; + CGFloat pageControlMaxX = self.sendButton.qmui_left; + CGFloat pageControlMinX = self.qmui_width - pageControlMaxX; + self.pageControl.frame = CGRectMake(pageControlMinX, self.qmui_height - self.safeAreaInsets.bottom - self.pageControlMarginBottom - pageControlHeight, pageControlMaxX - pageControlMinX, pageControlHeight); } - - self.sendButton.qmui_right = self.qmui_width - self.qmui_safeAreaInsets.right - self.sendButtonMargins.right; - self.sendButton.qmui_bottom = self.qmui_height - self.qmui_safeAreaInsets.bottom - self.sendButtonMargins.bottom; - - CGFloat pageControlHeight = 16; - CGFloat pageControlMaxX = self.sendButton.qmui_left; - CGFloat pageControlMinX = self.qmui_width - pageControlMaxX; - self.pageControl.frame = CGRectMake(pageControlMinX, self.qmui_height - self.qmui_safeAreaInsets.bottom - self.pageControlMarginBottom - pageControlHeight, pageControlMaxX - pageControlMinX, pageControlHeight); } - (void)pageEmotions { @@ -302,6 +493,16 @@ - (void)handlePageControlEvent:(UIPageControl *)pageControl { [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:pageControl.currentPage inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES]; } +- (void)adjustEmotionsAlpha { + CGFloat x = MIN(self.deleteButton.frame.origin.x, self.sendButton.frame.origin.x); + CGFloat y = MIN(self.deleteButton.frame.origin.y, self.sendButton.frame.origin.y); + CGFloat width = CGRectGetMaxX(self.sendButton.frame) - CGRectGetMinX(self.deleteButton.frame); + CGFloat height = MAX(CGRectGetMaxY(self.deleteButton.frame), CGRectGetMaxY(self.sendButton.frame)) - MIN(CGRectGetMinY(self.deleteButton.frame), CGRectGetMinY(self.sendButton.frame)); + CGRect buttonGruopRect = CGRectMake(x, y, width, height); + CGRect floatingRect = [self.verticalScrollView convertRect:buttonGruopRect fromView:self]; + [self.verticalScrollView adjustEmotionsAlphaWithFloatingRect:floatingRect]; +} + #pragma mark - UIAppearance Setter - (void)setSendButtonTitleAttributes:(NSDictionary *)sendButtonTitleAttributes { @@ -319,6 +520,36 @@ - (void)setSendButtonCornerRadius:(CGFloat)sendButtonCornerRadius { self.sendButton.layer.cornerRadius = _sendButtonCornerRadius; } +- (void)setDeleteButtonBackgroundColor:(UIColor *)deleteButtonBackgroundColor { + _deleteButtonBackgroundColor = deleteButtonBackgroundColor; + self.deleteButton.backgroundColor = deleteButtonBackgroundColor; +} + +- (void)setDeleteButtonImage:(UIImage *)deleteButtonImage { + _deleteButtonImage = deleteButtonImage; + [self.deleteButton setImage:self.deleteButtonImage forState:UIControlStateNormal]; +} + +- (void)setDeleteButtonCornerRadius:(CGFloat)deleteButtonCornerRadius { + _deleteButtonCornerRadius = deleteButtonCornerRadius; + self.deleteButton.layer.cornerRadius = deleteButtonCornerRadius; +} + +#pragma mark - + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + if (scrollView == self.verticalScrollView) { + [self adjustEmotionsAlpha]; + } else if (scrollView == self.collectionView) { + CGFloat index = scrollView.contentOffset.x / scrollView.bounds.size.width; + if (ceil(index) == floor(index)) { + // 滚到到整页,需要调用 updateDeleteButton: 重新设置一次删除按钮,否则有可能是截图按钮 + QMUIEmotionPageView *pageView = (QMUIEmotionPageView *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]]; + [pageView updateDeleteButton:self.deleteButton]; + } + } +} + #pragma mark - - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { @@ -334,8 +565,8 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cell pageView.emotionSize = self.emotionSize; pageView.emotionSelectedBackgroundExtension = self.emotionSelectedBackgroundExtension; pageView.minimumEmotionHorizontalSpacing = self.minimumEmotionHorizontalSpacing; - [pageView.deleteButton setImage:self.deleteButtonImage forState:UIControlStateNormal]; - [pageView.deleteButton setImage:[self.deleteButtonImage qmui_imageWithAlpha:ButtonHighlightedAlpha] forState:UIControlStateHighlighted]; + [pageView updateDeleteButton:self.deleteButton]; + pageView.deleteButtonOffset = self.deleteButtonOffset; pageView.debug = self.debug; [pageView setNeedsDisplay]; return pageView; @@ -357,12 +588,18 @@ - (void)emotionPageView:(QMUIEmotionPageView *)emotionPageView didSelectEmotion: } } -- (void)didSelectDeleteButtonInEmotionPageView:(QMUIEmotionPageView *)emotionPageView { - if (self.didSelectDeleteButtonBlock) { - self.didSelectDeleteButtonBlock(); +- (void)emotionPageViewDidLayoutEmotions:(QMUIEmotionPageView *)emotionPageView { + if (self.verticalAlignment) { + [self adjustEmotionsAlpha]; } } +#pragma mark - Getter + +- (UIScrollView *)scrollView { + return self.verticalScrollView; +} + @end @interface QMUIEmotionView (UIAppearance) @@ -380,7 +617,7 @@ + (void)initialize { + (void)setDefaultAppearance { QMUIEmotionView *appearance = [QMUIEmotionView appearance]; - appearance.backgroundColor = UIColorWhite; + appearance.backgroundColor = UIColorForBackground;// 如果先设置了 UIView.appearance.backgroundColor,再使用最传统的 method_exchangeImplementations 交换 UIView.setBackgroundColor 方法,则会 crash。QMUI 这里是在 +initialize 时设置的,业务如果要 hook -[UIView setBackgroundColor:] 则需要比 +initialize 更早才行 appearance.deleteButtonImage = [QMUIHelper imageWithName:@"QMUI_emotion_delete"]; appearance.paddingInPage = UIEdgeInsetsMake(18, 18, 65, 18); appearance.numberOfRowsPerPage = 4; @@ -392,6 +629,8 @@ + (void)setDefaultAppearance { appearance.sendButtonCornerRadius = 4; appearance.sendButtonMargins = UIEdgeInsetsMake(0, 0, 16, 16); appearance.pageControlMarginBottom = 22; + appearance.deleteButtonCornerRadius = 4; + appearance.emotionVerticalSpacing = 10; UIPageControl *pageControlAppearance = [UIPageControl appearanceWhenContainedInInstancesOfClasses:@[[QMUIEmotionView class]]]; pageControlAppearance.pageIndicatorTintColor = UIColorMake(210, 210, 210); diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.h b/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.h index 4797cfd8..c4d74647 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIEmptyView.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.m b/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.m index f637fa44..ad7fec75 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIEmptyView.m @@ -19,6 +19,7 @@ #import "NSParagraphStyle+QMUI.h" #import "UIView+QMUI.h" #import "QMUIButton.h" +#import "QMUIAppearance.h" @interface QMUIEmptyView () @@ -45,24 +46,10 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { - (void)didInitialize { // 系统默认会在view即将被add到window上时才设置这些值,这个时机有点晚了,因为我们可能在add到window之前就进行sizeThatFits计算或对view进行截图等操作,因此这里提前到init时就去做 - QMUIEmptyView *appearance = [QMUIEmptyView appearance]; - _imageViewInsets = appearance.imageViewInsets; - _loadingViewInsets = appearance.loadingViewInsets; - _textLabelInsets = appearance.textLabelInsets; - _detailTextLabelInsets = appearance.detailTextLabelInsets; - _actionButtonInsets = appearance.actionButtonInsets; - _verticalOffset = appearance.verticalOffset; - _textLabelFont = appearance.textLabelFont; - _detailTextLabelFont = appearance.detailTextLabelFont; - _actionButtonFont = appearance.actionButtonFont; - _textLabelTextColor = appearance.textLabelTextColor; - _detailTextLabelTextColor = appearance.detailTextLabelTextColor; - _actionButtonTitleColor = appearance.actionButtonTitleColor; + [self qmui_applyAppearance]; self.scrollView = [[UIScrollView alloc] init]; - if (@available(iOS 11, *)) { - self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } + self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; self.scrollView.showsVerticalScrollIndicator = NO; self.scrollView.showsHorizontalScrollIndicator = NO; self.scrollView.scrollsToTop = NO; @@ -110,7 +97,7 @@ - (void)layoutSubviews { self.contentView.frame = CGRectSetY(self.contentView.frame, 0); } - self.scrollView.contentSize = CGSizeMake(fmax(CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(self.scrollView.contentInset), contentViewSize.width), fmax(CGRectGetHeight(self.scrollView.bounds) - UIEdgeInsetsGetVerticalValue(self.scrollView.contentInset), CGRectGetMaxY(self.contentView.frame))); + self.scrollView.contentSize = CGSizeMake(MAX(CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(self.scrollView.contentInset), contentViewSize.width), MAX(CGRectGetHeight(self.scrollView.bounds) - UIEdgeInsetsGetVerticalValue(self.scrollView.contentInset), CGRectGetMaxY(self.contentView.frame))); CGFloat originY = 0; @@ -144,27 +131,25 @@ - (void)layoutSubviews { - (CGSize)sizeThatContentViewFits { CGFloat resultWidth = CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(self.scrollView.contentInset); - - CGFloat imageViewHeight = [self.imageView sizeThatFits:CGSizeMake(resultWidth, CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.imageViewInsets); - CGFloat loadingViewHeight = CGRectGetHeight(self.loadingView.bounds) + UIEdgeInsetsGetVerticalValue(self.loadingViewInsets); - CGFloat textLabelHeight = [self.textLabel sizeThatFits:CGSizeMake(resultWidth, CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.textLabelInsets); - CGFloat detailTextLabelHeight = [self.detailTextLabel sizeThatFits:CGSizeMake(resultWidth, CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.detailTextLabelInsets); - CGFloat actionButtonHeight = [self.actionButton sizeThatFits:CGSizeMake(resultWidth, CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.actionButtonInsets); - CGFloat resultHeight = 0; if (!self.imageView.hidden) { + CGFloat imageViewHeight = [self.imageView sizeThatFits:CGSizeMake(resultWidth - UIEdgeInsetsGetHorizontalValue(self.imageViewInsets), CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.imageViewInsets); resultHeight += imageViewHeight; } if (!self.loadingView.hidden) { + CGFloat loadingViewHeight = CGRectGetHeight(self.loadingView.bounds) + UIEdgeInsetsGetVerticalValue(self.loadingViewInsets); resultHeight += loadingViewHeight; } if (!self.textLabel.hidden) { + CGFloat textLabelHeight = [self.textLabel sizeThatFits:CGSizeMake(resultWidth - UIEdgeInsetsGetHorizontalValue(self.textLabelInsets), CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.textLabelInsets); resultHeight += textLabelHeight; } if (!self.detailTextLabel.hidden) { + CGFloat detailTextLabelHeight = [self.detailTextLabel sizeThatFits:CGSizeMake(resultWidth - UIEdgeInsetsGetHorizontalValue(self.detailTextLabelInsets), CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.detailTextLabelInsets); resultHeight += detailTextLabelHeight; } if (!self.actionButton.hidden) { + CGFloat actionButtonHeight = [self.actionButton sizeThatFits:CGSizeMake(resultWidth - UIEdgeInsetsGetHorizontalValue(self.actionButtonInsets), CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.actionButtonInsets); resultHeight += actionButtonHeight; } diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.h b/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.h index 7b547ae5..a362bbb2 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIFloatLayoutView.h @@ -43,7 +43,7 @@ extern const CGSize QMUIFloatLayoutViewAutomaticalMaximumItemSize; /** * item 之间的间距,默认为 UIEdgeInsetsZero。 * - * @warning 上、下、左、右四个边缘的 item 布局时不会考虑 itemMargins.left/bottom/left/right。 + * @warning 上、下、左、右四个边缘的 item 布局时不会考虑 itemMargins.top/bottom/left/right。 */ @property(nonatomic, assign) UIEdgeInsets itemMargins; @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.m b/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.m index 56677993..2bfa426a 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIFloatLayoutView.m @@ -63,37 +63,33 @@ - (CGSize)layoutSubviewsWithSize:(CGSize)size shouldLayout:(BOOL)shouldLayout { CGPoint itemViewOrigin = CGPointMake(ValueSwitchAlignLeftOrRight(self.padding.left, size.width - self.padding.right), self.padding.top); CGFloat currentRowMaxY = itemViewOrigin.y; CGSize maximumItemSize = CGSizeEqualToSize(self.maximumItemSize, QMUIFloatLayoutViewAutomaticalMaximumItemSize) ? CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(self.padding), size.height - UIEdgeInsetsGetVerticalValue(self.padding)) : self.maximumItemSize; - - for (NSInteger i = 0, l = visibleItemViews.count; i < l; i ++) { + NSInteger line = -1; + for (NSInteger i = 0, l = visibleItemViews.count; i < l; i++) { UIView *itemView = visibleItemViews[i]; + CGRect itemViewFrame; CGSize itemViewSize = [itemView sizeThatFits:maximumItemSize]; - itemViewSize.width = fmax(self.minimumItemSize.width, itemViewSize.width); - itemViewSize.height = fmax(self.minimumItemSize.height, itemViewSize.height); - itemViewSize.width = fmin(maximumItemSize.width, itemViewSize.width); - itemViewSize.height = fmin(maximumItemSize.height, itemViewSize.height); + itemViewSize.width = MIN(maximumItemSize.width, MAX(self.minimumItemSize.width, itemViewSize.width)); + itemViewSize.height = MIN(maximumItemSize.height, MAX(self.minimumItemSize.height, itemViewSize.height)); BOOL shouldBreakline = i == 0 ? YES : ValueSwitchAlignLeftOrRight(itemViewOrigin.x + self.itemMargins.left + itemViewSize.width + self.padding.right > size.width, itemViewOrigin.x - self.itemMargins.right - itemViewSize.width - self.padding.left < 0); if (shouldBreakline) { + line++; + currentRowMaxY += line > 0 ? self.itemMargins.top : 0; // 换行,每一行第一个 item 是不考虑 itemMargins 的 - if (shouldLayout) { - itemView.frame = CGRectMake(ValueSwitchAlignLeftOrRight(self.padding.left, size.width - self.padding.right - itemViewSize.width), currentRowMaxY + self.itemMargins.top, itemViewSize.width, itemViewSize.height); - } - - itemViewOrigin.x = ValueSwitchAlignLeftOrRight(self.padding.left + itemViewSize.width + self.itemMargins.right, size.width - self.padding.right - itemViewSize.width - self.itemMargins.left); - itemViewOrigin.y = currentRowMaxY; + itemViewFrame = CGRectMake(ValueSwitchAlignLeftOrRight(self.padding.left, size.width - self.padding.right - itemViewSize.width), currentRowMaxY, itemViewSize.width, itemViewSize.height); + itemViewOrigin.y = CGRectGetMinY(itemViewFrame); } else { // 当前行放得下 - if (shouldLayout) { - itemView.frame = CGRectMake(ValueSwitchAlignLeftOrRight(itemViewOrigin.x + self.itemMargins.left, itemViewOrigin.x - self.itemMargins.right - itemViewSize.width), itemViewOrigin.y + self.itemMargins.top, itemViewSize.width, itemViewSize.height); - } - - itemViewOrigin.x = ValueSwitchAlignLeftOrRight(itemViewOrigin.x + UIEdgeInsetsGetHorizontalValue(self.itemMargins) + itemViewSize.width, - itemViewOrigin.x - itemViewSize.width - UIEdgeInsetsGetHorizontalValue(self.itemMargins)); + itemViewFrame = CGRectMake(ValueSwitchAlignLeftOrRight(itemViewOrigin.x + self.itemMargins.left, itemViewOrigin.x - self.itemMargins.right - itemViewSize.width), itemViewOrigin.y, itemViewSize.width, itemViewSize.height); } + itemViewOrigin.x = ValueSwitchAlignLeftOrRight(CGRectGetMaxX(itemViewFrame) + self.itemMargins.right, CGRectGetMinX(itemViewFrame) - self.itemMargins.left); + currentRowMaxY = MAX(currentRowMaxY, CGRectGetMaxY(itemViewFrame) + self.itemMargins.bottom); - currentRowMaxY = fmax(currentRowMaxY, itemViewOrigin.y + UIEdgeInsetsGetVerticalValue(self.itemMargins) + itemViewSize.height); + if (shouldLayout) { + itemView.frame = itemViewFrame; + } } // 最后一行不需要考虑 itemMarins.bottom,所以这里减掉 diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIGridView.h b/QMUI/QMUIKit/QMUIComponents/QMUIGridView.h index 3319ac65..80e35b7a 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIGridView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIGridView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIGridView.h @@ -30,6 +30,9 @@ /// 指定每一行的高度,默认为 0 @property(nonatomic, assign) IBInspectable CGFloat rowHeight; +/// 内部的 padding,默认为 UIEdgeInsetsZero +@property(nonatomic, assign) UIEdgeInsets padding; + /// 指定 item 之间的分隔线宽度,默认为 0 @property(nonatomic, assign) IBInspectable CGFloat separatorWidth; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIGridView.m b/QMUI/QMUIKit/QMUIComponents/QMUIGridView.m index 408b35b5..b8214088 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIGridView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIGridView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIGridView.m @@ -70,7 +70,7 @@ - (void)setSeparatorColor:(UIColor *)separatorColor { // 返回最接近平均列宽的值,保证其为整数,因此所有columnWidth加起来可能比总宽度要小 - (CGFloat)stretchColumnWidth { - return floor((CGRectGetWidth(self.bounds) - self.separatorWidth * (self.columnCount - 1)) / self.columnCount); + return floor((CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.padding) - self.separatorWidth * (self.columnCount - 1)) / self.columnCount); } - (NSInteger)rowCount { @@ -81,6 +81,7 @@ - (NSInteger)rowCount { - (CGSize)sizeThatFits:(CGSize)size { NSInteger rowCount = [self rowCount]; CGFloat totalHeight = rowCount * self.rowHeight + (rowCount - 1) * self.separatorWidth; + totalHeight += UIEdgeInsetsGetVerticalValue(self.padding); size.height = totalHeight; return size; } @@ -109,15 +110,15 @@ - (void)layoutSubviews { BOOL isLastRow = row == rowCount - 1; UIView *subview = self.subviews[index]; - CGRect subviewFrame = CGRectMake(columnWidth * column + self.separatorWidth * column, rowHeight * row + self.separatorWidth * row, columnWidth, rowHeight); + CGRect subviewFrame = CGRectMake(columnWidth * column + self.separatorWidth * column + self.padding.left, rowHeight * row + self.separatorWidth * row + self.padding.top, columnWidth, rowHeight); if (isLastColumn) { // 每行最后一个item要占满剩余空间,否则可能因为strecthColumnWidth不精确导致右边漏空白 - subviewFrame.size.width = size.width - columnWidth * (self.columnCount - 1) - self.separatorWidth * (self.columnCount - 1); + subviewFrame.size.width = size.width - UIEdgeInsetsGetHorizontalValue(self.padding) - columnWidth * (self.columnCount - 1) - self.separatorWidth * (self.columnCount - 1); } if (isLastRow) { // 最后一行的item要占满剩余空间,避免一些计算偏差 - subviewFrame.size.height = size.height - rowHeight * (rowCount - 1) - self.separatorWidth * (rowCount - 1); + subviewFrame.size.height = size.height - UIEdgeInsetsGetVerticalValue(self.padding) - rowHeight * (rowCount - 1) - self.separatorWidth * (rowCount - 1); } subview.frame = subviewFrame; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.h b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.h index 583e95a0..85838dc0 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIImagePreviewView.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m index fb5ee203..9aa70d68 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIImagePreviewView.m @@ -83,17 +83,15 @@ - (void)didInitializedWithFrame:(CGRect)frame { self.collectionViewLayout.allowsMultipleItemScroll = NO; _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMakeWithSize(frame.size) collectionViewLayout:self.collectionViewLayout]; - self.collectionView.delegate = self; self.collectionView.dataSource = self; + self.collectionView.delegate = self; self.collectionView.backgroundColor = UIColorClear; self.collectionView.showsHorizontalScrollIndicator = NO; self.collectionView.showsVerticalScrollIndicator = NO; self.collectionView.scrollsToTop = NO; self.collectionView.delaysContentTouches = NO; self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast; - if (@available(iOS 11, *)) { - self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } + self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; [self.collectionView registerClass:[QMUIImagePreviewCell class] forCellWithReuseIdentifier:kImageOrUnknownCellIdentifier]; [self.collectionView registerClass:[QMUIImagePreviewCell class] forCellWithReuseIdentifier:kVideoCellIdentifier]; [self.collectionView registerClass:[QMUIImagePreviewCell class] forCellWithReuseIdentifier:kLivePhotoCellIdentifier]; @@ -169,9 +167,7 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cell // 因为 cell 复用的问题,很可能此时会显示一张错误的图片,因此这里要清空所有图片的显示 zoomView.image = nil; zoomView.videoPlayerItem = nil; - if (@available(iOS 9.1, *)) { - zoomView.livePhoto = nil; - } + zoomView.livePhoto = nil; if ([self.delegate respondsToSelector:@selector(imagePreviewView:renderZoomImageView:atIndex:)]) { [self.delegate imagePreviewView:self renderZoomImageView:zoomView atIndex:indexPath.item]; } @@ -221,8 +217,15 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { // 在滑动过临界点的那一次才去调用 delegate,避免过于频繁的调用 BOOL isFirstDidScroll = self.previousIndexWhenScrolling == 0; - BOOL turnPageToRight = betweenOrEqual(self.previousIndexWhenScrolling, floor(index) + 0.5, index); - BOOL turnPageToLeft = betweenOrEqual(index, floor(index) + 0.5, self.previousIndexWhenScrolling); + + // fastToRight example : self.previousIndexWhenScrolling 1.49, index = 2.0 + BOOL fastToRight = (floor(index) - floor(self.previousIndexWhenScrolling) >= 1.0) && (floor(index) - self.previousIndexWhenScrolling > 0.5); + BOOL turnPageToRight = fastToRight || betweenOrEqual(self.previousIndexWhenScrolling, floor(index) + 0.5, index); + + // fastToLeft example : self.previousIndexWhenScrolling 2.51, index = 1.99 + BOOL fastToLeft = (floor(self.previousIndexWhenScrolling) - floor(index) >= 1.0) && (self.previousIndexWhenScrolling - ceil(index) > 0.5); + BOOL turnPageToLeft = fastToLeft || betweenOrEqual(index, floor(index) + 0.5, self.previousIndexWhenScrolling); + if (!isFirstDidScroll && (turnPageToRight || turnPageToLeft)) { index = round(index); if (0 <= index && index < [self.collectionView numberOfItemsInSection:0]) { @@ -246,7 +249,7 @@ - (NSInteger)indexForZoomImageView:(QMUIZoomImageView *)zoomImageView { if ([zoomImageView.superview.superview isKindOfClass:[QMUIImagePreviewCell class]]) { return [self.collectionView indexPathForCell:(QMUIImagePreviewCell *)zoomImageView.superview.superview].item; } else { - NSAssert(NO, @"尝试通过 %s 获取 QMUIZoomImageView 所在的 index,但找不到 QMUIZoomImageView 所在的 cell,index 获取失败。%@", __func__, zoomImageView); + QMUIAssert(NO, @"QMUIImagePreviewView (QMUIZoomImageView)", @"尝试通过 %s 获取 QMUIZoomImageView 所在的 index,但找不到 QMUIZoomImageView 所在的 cell,index 获取失败。%@", __func__, zoomImageView); } return NSNotFound; } @@ -260,7 +263,7 @@ - (void)checkIfDelegateMissing { #ifdef DEBUG [NSObject qmui_enumerateProtocolMethods:@protocol(QMUIZoomImageViewDelegate) usingBlock:^(SEL selector) { if (![self respondsToSelector:selector]) { - NSAssert(NO, @"%@ 需要响应 %@ 的方法 -%@", NSStringFromClass([self class]), NSStringFromProtocol(@protocol(QMUIZoomImageViewDelegate)), NSStringFromSelector(selector)); + QMUIAssert(NO, @"QMUIImagePreviewView (QMUIZoomImageView)", @"%@ 需要响应 %@ 的方法 -%@", NSStringFromClass([self class]), NSStringFromProtocol(@protocol(QMUIZoomImageViewDelegate)), NSStringFromSelector(selector)); } }]; #endif diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.h b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.h index 873832dd..be6ff859 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIImagePreviewViewController.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m index 3775a826..167572ab 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIImagePreviewViewController.m @@ -19,28 +19,25 @@ #import "UIInterface+QMUI.h" #import "UIView+QMUI.h" #import "UIViewController+QMUI.h" +#import "QMUIAppearance.h" const CGFloat QMUIImagePreviewViewControllerCornerRadiusAutomaticDimension = -1; @implementation QMUIImagePreviewViewController (UIAppearance) ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; +} + + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - [self appearance]; + [self initAppearance]; }); } -static QMUIImagePreviewViewController *imagePreviewViewControllerAppearance; -+ (nonnull instancetype)appearance { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - if (!imagePreviewViewControllerAppearance) { - imagePreviewViewControllerAppearance = [[QMUIImagePreviewViewController alloc] init]; - imagePreviewViewControllerAppearance.backgroundColor = UIColorBlack; - } - }); - return imagePreviewViewControllerAppearance; ++ (void)initAppearance { + QMUIImagePreviewViewController.appearance.backgroundColor = UIColorBlack; } @end @@ -57,19 +54,19 @@ @interface QMUIImagePreviewViewController () @implementation QMUIImagePreviewViewController -@synthesize imagePreviewView = _imagePreviewView; - - (void)didInitialize { [super didInitialize]; - self.automaticallyAdjustsScrollViewInsets = NO; self.sourceImageCornerRadius = QMUIImagePreviewViewControllerCornerRadiusAutomaticDimension; _dismissingGestureEnabled = YES; - if (imagePreviewViewControllerAppearance) { - self.backgroundColor = [QMUIImagePreviewViewController appearance].backgroundColor; - } + [self qmui_applyAppearance]; + + self.qmui_prefersHomeIndicatorAutoHiddenBlock = ^BOOL{ + return YES; + }; + // present style self.transitioningAnimator = [[QMUIImagePreviewViewTransitionAnimator alloc] init]; @@ -78,11 +75,6 @@ - (void)didInitialize { self.transitioningDelegate = self; } -- (QMUIImagePreviewView *)imagePreviewView { - [self loadViewIfNeeded]; - return _imagePreviewView; -} - - (void)setBackgroundColor:(UIColor *)backgroundColor { _backgroundColor = backgroundColor; if ([self isViewLoaded]) { @@ -90,14 +82,17 @@ - (void)setBackgroundColor:(UIColor *)backgroundColor { } } -- (void)viewDidLoad { - [super viewDidLoad]; - self.view.backgroundColor = self.backgroundColor; +@synthesize imagePreviewView = _imagePreviewView; +- (QMUIImagePreviewView *)imagePreviewView { + if (!_imagePreviewView) { + _imagePreviewView = [[QMUIImagePreviewView alloc] initWithFrame:self.isViewLoaded ? self.view.bounds : CGRectZero]; + } + return _imagePreviewView; } - (void)initSubviews { [super initSubviews]; - _imagePreviewView = [[QMUIImagePreviewView alloc] initWithFrame:self.view.bounds]; + self.view.backgroundColor = self.backgroundColor; [self.view addSubview:self.imagePreviewView]; } @@ -152,7 +147,8 @@ - (BOOL)prefersStatusBarHidden { if (self.qmui_visibleState < QMUIViewControllerDidAppear || self.qmui_visibleState >= QMUIViewControllerDidDisappear) { // 在 present/dismiss 动画过程中,都使用原界面的状态栏显隐状态 if (self.presentingViewController) { - self.originalStatusBarHidden = self.presentingViewController.qmui_prefersStatusBarHidden; + BOOL statusBarHidden = self.presentingViewController.view.window.windowScene.statusBarManager.statusBarHidden; + self.originalStatusBarHidden = statusBarHidden; return self.originalStatusBarHidden; } return [super prefersStatusBarHidden]; @@ -160,10 +156,6 @@ - (BOOL)prefersStatusBarHidden { return self.statusBarHidden; } -- (BOOL)prefersHomeIndicatorAutoHidden { - return YES; -} - #pragma mark - 动画 - (void)initObjectsForZoomStyleIfNeeded { diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.h b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.h index fa32be33..543aa393 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.h @@ -1,17 +1,16 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIImagePreviewViewTransitionAnimator.h // QMUIKit // // Created by MoLice on 2018/D/19. -// Copyright © 2018 QMUI Team. All rights reserved. // #import diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.m b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.m index 6ade5cc3..f8c12b26 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.m @@ -1,17 +1,16 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIImagePreviewViewTransitionAnimator.m // QMUIKit // // Created by MoLice on 2018/D/19. -// Copyright © 2018 QMUI Team. All rights reserved. // #import "QMUIImagePreviewViewTransitionAnimator.h" @@ -178,7 +177,11 @@ - (void)animateTransition:(nonnull id)tran UIView *containerView = transitionContext.containerView; UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey]; + [fromView setNeedsLayout]; + [fromView layoutIfNeeded]; UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey]; + [toView setNeedsLayout]; + [toView layoutIfNeeded];// present 时 toViewController 还没走到 viewDidLayoutSubviews,此时做动画可能得到不正确的布局,所以强制布局一次 QMUIZoomImageView *zoomImageView = [self.imagePreviewViewController.imagePreviewView zoomImageViewAtIndex:self.imagePreviewViewController.imagePreviewView.currentImageIndex]; toView.frame = containerView.bounds; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.h b/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.h index ac546230..b7da3997 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIKeyboardManager.h @@ -19,12 +19,11 @@ @protocol QMUIKeyboardManagerDelegate; @class QMUIKeyboardUserInfo; -/// 注意:由于某些Bug(例如 iOS 8 的 iPad 修改切换键盘类型,delegate 回调键盘高度值错误),QMUIKeyboardManager 不再支持 iPad 的浮动键盘了 - 更新于 2017.12.8 /// -/// 注意:QMUI 已经废弃 iOS8 了,所以浮动键盘又可以支持了 - /** - * `QMUIKeyboardManager` 提供了方便管理键盘事件的方案,使用的场景是需要跟随键盘的显示或者隐藏来更改界面的 UI,例如输入框跟随在键盘的顶部。 + * `QMUIKeyboardManager` 提供了方便、稳定的管理键盘事件的方案,使用的场景是需要跟随键盘的显示或者隐藏来更改界面的 UI,例如输入框跟随在键盘的顶部。 * 由于键盘通知是整个 App 全局的,所以经常会遇到 A 的键盘监听回调里接收到 B 的键盘事件,这样的情况往往不是我们想要的,即使可以通过判断当前的 firstResponder 来区分,但还是不能完美的解决问题或者有时候解决起来非常麻烦。`QMUIKeyboardManager` 通过 `delegateEnabled` 和 `targetResponder` 等属性来方便地控制 firstResponder,从而可以实现某个键盘监听回调方法只响应某个 UIResponder 或者某几个 UIResponder 触发的键盘通知。 + * 另外系统的“设置→辅助功能→动态效果→减弱动态效果→首选交叉淡出过渡效果”会改变系统键盘的动画,QMUIKeyboardManager 也兼容了这种情况,如果业务自己处理,很容易会遗漏。 + * * 使用方式: * 1. 使用 initWithDelegate: 方法初始化 * 2. 通过 addTargetResponder: 的方式将要监听的输入框添加进来 @@ -52,6 +51,14 @@ */ @property(nonatomic, assign) BOOL delegateEnabled; +/** + * 是否忽视 `applicationState` 状态的影响。默认为 NO,也即只有 `UIApplicationStateActive` 才会响应通知,如果设置为 YES,则任何 state 都会响应通知。 + */ +@property(nonatomic, assign) BOOL ignoreApplicationState UI_APPEARANCE_SELECTOR; + + ++ (instancetype)appearance; + /** * 添加触发键盘事件的 UIResponder,一般是 UITextView 或者 UITextField ,不添加 targetResponder 的话,则默认接受任何 UIResponder 产生的键盘通知。 * 添加成功将会返回YES,否则返回NO。 @@ -182,6 +189,11 @@ */ @property(nonatomic, assign, readonly) UIViewAnimationOptions animationOptions; +/** + * 当前是否浮动键盘 + */ +@property(nonatomic, assign, readonly) BOOL isFloatingKeyboard; + @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.m b/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.m index cc04c0a5..178df3ec 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIKeyboardManager.m @@ -16,11 +16,18 @@ #import "QMUIKeyboardManager.h" #import "QMUICore.h" #import "QMUILog.h" +#import "QMUIAppearance.h" +#import "QMUIMultipleDelegates.h" +#import "NSArray+QMUI.h" +#import "UIView+QMUI.h" + +@class QMUIKeyboardViewFrameObserver; +@protocol QMUIKeyboardViewFrameObserverDelegate +@required +- (void)keyboardViewFrameDidChange:(UIView *)keyboardView; +@end - -static QMUIKeyboardManager *kKeyboardManagerInstance; - -@interface QMUIKeyboardManager () +@interface QMUIKeyboardManager () @property(nonatomic, strong) NSMutableArray *targetResponderValues; @@ -28,7 +35,7 @@ @interface QMUIKeyboardManager () @property(nonatomic, assign) CGRect keyboardMoveBeginRect; @property(nonatomic, weak) UIResponder *currentResponder; -@property(nonatomic, weak) UIResponder *currentResponderWhenResign; +//@property(nonatomic, weak) UIResponder *currentResponderWhenResign; @property(nonatomic, assign) BOOL debug; @@ -74,10 +81,7 @@ + (void)load { dispatch_once(&onceToken, ^{ OverrideImplementation([UIResponder class], @selector(becomeFirstResponder), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^BOOL(UIResponder *selfObject) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - selfObject.keyboardManager_isFirstResponder = YES; - } + selfObject.keyboardManager_isFirstResponder = YES; // call super BOOL (*originSelectorIMP)(id, SEL); @@ -90,16 +94,12 @@ + (void)load { OverrideImplementation([UIResponder class], @selector(resignFirstResponder), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^BOOL(UIResponder *selfObject) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - selfObject.keyboardManager_isFirstResponder = NO; - if (selfObject.isFirstResponder && - selfObject.qmui_keyboardManager && - [selfObject.qmui_keyboardManager.allTargetResponders containsObject:selfObject]) { - selfObject.qmui_keyboardManager.currentResponderWhenResign = selfObject; - } - } - + selfObject.keyboardManager_isFirstResponder = NO; +// if (selfObject.isFirstResponder && +// selfObject.qmui_keyboardManager && +// [selfObject.qmui_keyboardManager.allTargetResponders containsObject:selfObject]) { +// selfObject.qmui_keyboardManager.currentResponderWhenResign = selfObject; +// } // call super BOOL (*originSelectorIMP)(id, SEL); originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider(); @@ -116,7 +116,7 @@ + (void)load { @interface QMUIKeyboardViewFrameObserver : NSObject -@property (nonatomic, copy) void (^keyboardViewChangeFrameBlock)(UIView *keyboardView); +@property (nonatomic, weak) id delegate; - (void)addToKeyboardView:(UIView *)keyboardView; + (instancetype)observerForView:(UIView *)keyboardView; @@ -187,8 +187,8 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N } id newValue = [change objectForKey:NSKeyValueChangeNewKey]; if (newValue == [NSNull null]) { newValue = nil; } - if (self.keyboardViewChangeFrameBlock) { - self.keyboardViewChangeFrameBlock(_keyboardView); + if (self.delegate) { + [self.delegate keyboardViewFrameDidChange:_keyboardView]; } } @@ -212,6 +212,8 @@ @interface QMUIKeyboardUserInfo () @property(nonatomic, assign, readwrite) UIViewAnimationCurve animationCurve; @property(nonatomic, assign, readwrite) UIViewAnimationOptions animationOptions; +@property(nonatomic, assign, readwrite) BOOL isFloatingKeyboard; + @end @implementation QMUIKeyboardUserInfo @@ -219,11 +221,26 @@ @implementation QMUIKeyboardUserInfo - (void)setNotification:(NSNotification *)notification { _notification = notification; if (self.originUserInfo) { + _animationDuration = [[self.originUserInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + _animationCurve = (UIViewAnimationCurve)[[self.originUserInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue]; + _animationOptions = self.animationCurve<<16; - _beginFrame = [[self.originUserInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue]; - _endFrame = [[self.originUserInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; + + CGRect beginFrame = [[self.originUserInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue]; + CGRect endFrame = [[self.originUserInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; + + // iOS 13 分屏键盘 x 不是 0,不知道是系统 BUG 还是故意这样,先这样保护,再观察一下后面的 beta 版本 + if (IS_SPLIT_SCREEN_IPAD && beginFrame.origin.x > 0) { + beginFrame.origin.x = 0; + } + if (IS_SPLIT_SCREEN_IPAD && endFrame.origin.x > 0) { + endFrame.origin.x = 0; + } + + _beginFrame = beginFrame; + _endFrame = endFrame; } } @@ -300,13 +317,8 @@ - (UIViewAnimationOptions)animationOptions { */ @implementation QMUIKeyboardManager -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - if (!kKeyboardManagerInstance) { - kKeyboardManagerInstance = [[QMUIKeyboardManager alloc] initWithDelegate:nil]; - } - }); ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; } - (instancetype)init { @@ -325,6 +337,7 @@ - (instancetype)initWithDelegate:(id )delegate { _delegateEnabled = YES; _targetResponderValues = [[NSMutableArray alloc] init]; [self addKeyboardNotification]; + [self qmui_applyAppearance]; } return self; } @@ -383,10 +396,10 @@ - (UIResponder *)unPackageTargetResponder:(NSValue *)value { } - (UIResponder *)firstResponderInWindows { - UIResponder *responder = [[UIApplication sharedApplication].keyWindow qmui_findFirstResponder]; + UIResponder *responder = [UIApplication.sharedApplication.keyWindow qmui_findFirstResponder]; if (!responder) { - for (UIWindow *window in [UIApplication sharedApplication].windows) { - if (window != [UIApplication sharedApplication].keyWindow) { + for (UIWindow *window in UIApplication.sharedApplication.windows) { + if (window != UIApplication.sharedApplication.keyWindow) { responder = [window qmui_findFirstResponder]; if (responder) { return responder; @@ -408,14 +421,24 @@ - (void)addKeyboardNotification { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidChangeFrameNotification:) name:UIKeyboardDidChangeFrameNotification object:nil]; } -- (BOOL)isAppActive:(NSNotification *)notification { - if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) { - return NO; +- (BOOL)isAppActive { + if (self.ignoreApplicationState) { + return YES; } - if (![[notification.userInfo valueForKey:UIKeyboardIsLocalUserInfoKey] boolValue]) { - return NO; + if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive) { + return YES; } - return YES; + return NO; +} + +- (BOOL)isLocalKeyboard:(NSNotification *)notification { + if ([[notification.userInfo valueForKey:UIKeyboardIsLocalUserInfoKey] boolValue]) { + return YES; + } + if (IS_SPLIT_SCREEN_IPAD) { + return YES; + } + return NO; } - (void)keyboardWillShowNotification:(NSNotification *)notification { @@ -424,7 +447,7 @@ - (void)keyboardWillShowNotification:(NSNotification *)notification { QMUILog(NSStringFromClass(self.class), @"keyboardWillShowNotification - %@", self); } - if (![self isAppActive:notification]) { + if (![self isAppActive] || ![self isLocalKeyboard:notification]) { QMUILog(NSStringFromClass(self.class), @"app is not active"); return; } @@ -453,7 +476,7 @@ - (void)keyboardDidShowNotification:(NSNotification *)notification { QMUILog(NSStringFromClass(self.class), @"keyboardDidShowNotification - %@", self); } - if (![self isAppActive:notification]) { + if (![self isAppActive] || ![self isLocalKeyboard:notification]) { QMUILog(NSStringFromClass(self.class), @"app is not active"); return; } @@ -482,7 +505,7 @@ - (void)keyboardWillHideNotification:(NSNotification *)notification { QMUILog(NSStringFromClass(self.class), @"keyboardWillHideNotification - %@", self); } - if (![self isAppActive:notification]) { + if (![self isAppActive] || ![self isLocalKeyboard:notification]) { QMUILog(NSStringFromClass(self.class), @"app is not active"); return; } @@ -511,7 +534,7 @@ - (void)keyboardDidHideNotification:(NSNotification *)notification { QMUILog(NSStringFromClass(self.class), @"keyboardDidHideNotification - %@", self); } - if (![self isAppActive:notification]) { + if (![self isAppActive] || ![self isLocalKeyboard:notification]) { QMUILog(NSStringFromClass(self.class), @"app is not active"); return; } @@ -527,6 +550,7 @@ - (void)keyboardDidHideNotification:(NSNotification *)notification { } if (self.currentResponder && !self.currentResponder.keyboardManager_isFirstResponder && !IS_IPAD) { + // 时机最晚,设置为 nil self.currentResponder = nil; } @@ -544,7 +568,7 @@ - (void)keyboardWillChangeFrameNotification:(NSNotification *)notification { QMUILog(NSStringFromClass(self.class), @"keyboardWillChangeFrameNotification - %@", self); } - if (![self isAppActive:notification]) { + if (![self isAppActive] || ![self isLocalKeyboard:notification]) { QMUILog(NSStringFromClass(self.class), @"app is not active"); return; } @@ -552,9 +576,7 @@ - (void)keyboardWillChangeFrameNotification:(NSNotification *)notification { QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification]; self.lastUserInfo = userInfo; - if ([self shouldReceiveShowNotification]) { - userInfo.targetResponder = self.currentResponder ?: nil; - } else if ([self shouldReceiveHideNotification]) { + if ([self shouldReceiveShowNotification] || [self shouldReceiveHideNotification]) { userInfo.targetResponder = self.currentResponder ?: nil; } else { return; @@ -576,7 +598,7 @@ - (void)keyboardDidChangeFrameNotification:(NSNotification *)notification { QMUILog(NSStringFromClass(self.class), @"keyboardDidChangeFrameNotification - %@", self); } - if (![self isAppActive:notification]) { + if (![self isAppActive] || ![self isLocalKeyboard:notification]) { QMUILog(NSStringFromClass(self.class), @"app is not active"); return; } @@ -584,9 +606,7 @@ - (void)keyboardDidChangeFrameNotification:(NSNotification *)notification { QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification]; self.lastUserInfo = userInfo; - if ([self shouldReceiveShowNotification]) { - userInfo.targetResponder = self.currentResponder ?: nil; - } else if ([self shouldReceiveHideNotification]) { + if ([self shouldReceiveShowNotification] || [self shouldReceiveHideNotification]) { userInfo.targetResponder = self.currentResponder ?: nil; } else { return; @@ -610,9 +630,13 @@ - (QMUIKeyboardUserInfo *)newUserInfoWithNotification:(NSNotification *)notifica } - (BOOL)shouldReceiveShowNotification { - // 这里有BUG,如果点击了webview导致键盘下降,这个时候运行shouldReceiveHideNotification就会判断错误 - self.currentResponder = self.currentResponderWhenResign ?: [self firstResponderInWindows]; - self.currentResponderWhenResign = nil; + UIResponder *firstResponder = [self firstResponderInWindows]; + // 如果点击了 webview 导致键盘下降,这个时候运行 shouldReceiveHideNotification 就会判断错误,所以如果发现是 nil 或是 WKContentView 则值不变 + // WKContentView + if (!self.currentResponder || (firstResponder && ![firstResponder isKindOfClass:NSClassFromString([NSString stringWithFormat:@"%@%@", @"WK", @"ContentView"])])) { + self.currentResponder = firstResponder; + } + if (self.targetResponderValues.count <= 0) { return YES; } else { @@ -638,16 +662,15 @@ - (void)addFrameObserverIfNeeded { if (![self.class keyboardView]) { return; } - __weak __typeof(self)weakSelf = self; - QMUIKeyboardViewFrameObserver *observer = [QMUIKeyboardViewFrameObserver observerForView:[self.class keyboardView]]; + UIView *keyboardView = [self.class keyboardView]; + QMUIKeyboardViewFrameObserver *observer = [QMUIKeyboardViewFrameObserver observerForView:keyboardView]; if (!observer) { observer = [[QMUIKeyboardViewFrameObserver alloc] init]; - observer.keyboardViewChangeFrameBlock = ^(UIView *keyboardView) { - [weakSelf keyboardDidChangedFrame:keyboardView]; - }; - [observer addToKeyboardView:[self.class keyboardView]]; - [self keyboardDidChangedFrame:[self.class keyboardView]]; // 手动调用第一次 + observer.qmui_multipleDelegatesEnabled = YES; + [observer addToKeyboardView:keyboardView]; } + observer.delegate = self; + [self keyboardDidChangedFrame:keyboardView]; // 手动调用第一次 } - (void)keyboardDidChangedFrame:(UIView *)keyboardView { @@ -686,10 +709,10 @@ - (void)keyboardDidChangedFrame:(UIView *)keyboardView { keyboardMoveUserInfo.animationOptions = self.lastUserInfo ? self.lastUserInfo.animationOptions : keyboardMoveUserInfo.animationCurve<<16; keyboardMoveUserInfo.beginFrame = self.keyboardMoveBeginRect; keyboardMoveUserInfo.endFrame = endFrame; + keyboardMoveUserInfo.isFloatingKeyboard = keyboardView ? CGRectGetWidth(keyboardView.bounds) < CGRectGetWidth(UIApplication.sharedApplication.delegate.window.bounds) : NO; if (self.debug) { - NSLog(@"keyboardDidMoveNotification - %@", self); - NSLog(@"\n"); + NSLog(@"keyboardDidMoveNotification - %@\n", self); } [self.delegate keyboardWillChangeFrameWithUserInfo:keyboardMoveUserInfo]; @@ -697,7 +720,7 @@ - (void)keyboardDidChangedFrame:(UIView *)keyboardView { self.keyboardMoveBeginRect = endFrame; if (self.currentResponder) { - UIWindow *mainWindow = [UIApplication sharedApplication].keyWindow ?: [[UIApplication sharedApplication] windows].firstObject; + UIWindow *mainWindow = UIApplication.sharedApplication.keyWindow ?: UIApplication.sharedApplication.delegate.window; if (mainWindow) { CGRect keyboardRect = keyboardMoveUserInfo.endFrame; CGFloat distanceFromBottom = [QMUIKeyboardManager distanceFromMinYToBottomInView:mainWindow keyboardRect:keyboardRect]; @@ -718,6 +741,12 @@ - (void)keyboardDidChangedFrame:(UIView *)keyboardView { } } +#pragma mark - + +- (void)keyboardViewFrameDidChange:(UIView *)keyboardView { + [self keyboardDidChangedFrame:keyboardView]; +} + #pragma mark - 工具方法 + (void)animateWithAnimated:(BOOL)animated keyboardUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion { @@ -744,9 +773,12 @@ + (void)animateWithAnimated:(BOOL)animated keyboardUserInfo:(QMUIKeyboardUserInf + (void)handleKeyboardNotificationWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo showBlock:(void (^)(QMUIKeyboardUserInfo *keyboardUserInfo))showBlock hideBlock:(void (^)(QMUIKeyboardUserInfo *keyboardUserInfo))hideBlock { // 专门处理 iPad Pro 在键盘完全不显示的情况(不会调用willShow,所以通过是否focus来判断) // iPhoneX Max 这里键盘高度不是0,而是一个很小的值 - if ([QMUIKeyboardManager visibleKeyboardHeight] <= 0 && !keyboardUserInfo.isTargetResponderFocused) { - if (hideBlock) { - hideBlock(keyboardUserInfo); + if (!keyboardUserInfo.isTargetResponderFocused) { + // 先判断 focus,避免 frame 变化但是此时 visibleKeyboardHeight 还不是 0 导致调用了 showBlock + if ([QMUIKeyboardManager visibleKeyboardHeight] <= 0) { + if (hideBlock) { + hideBlock(keyboardUserInfo); + } } } else { if (showBlock) { @@ -755,39 +787,13 @@ + (void)handleKeyboardNotificationWithUserInfo:(QMUIKeyboardUserInfo *)keyboardU } } -+ (UIWindow *)keyboardWindow { - - for (UIWindow *window in [UIApplication sharedApplication].windows) { - if ([self getKeyboardViewFromWindow:window]) { - return window; - } - } - - NSMutableArray *kbWindows = nil; - - for (UIWindow *window in [UIApplication sharedApplication].windows) { - NSString *windowName = NSStringFromClass(window.class); - if (windowName.length == 22 && [windowName hasPrefix:@"UI"] && [windowName hasSuffix:[NSString stringWithFormat:@"%@%@", @"Remote", @"KeyboardWindow"]]) { - // UIRemoteKeyboardWindow(iOS9 以下 UITextEffectsWindow) - if (!kbWindows) kbWindows = [NSMutableArray new]; - [kbWindows addObject:window]; - } - } - - if (kbWindows.count == 1) { - return kbWindows.firstObject; - } - - return nil; -} - + (CGRect)convertKeyboardRect:(CGRect)rect toView:(UIView *)view { if (CGRectIsNull(rect) || CGRectIsInfinite(rect)) { return rect; } - UIWindow *mainWindow = [UIApplication sharedApplication].keyWindow ?: [UIApplication sharedApplication].windows.firstObject; + UIWindow *mainWindow = UIApplication.sharedApplication.keyWindow ?: UIApplication.sharedApplication.delegate.window; if (!mainWindow) { if (view) { [view convertRect:rect fromView:nil]; @@ -825,40 +831,71 @@ + (CGFloat)distanceFromMinYToBottomInView:(UIView *)view keyboardRect:(CGRect)re return distance; } +/** + 从所有 window 里寻找代表键盘当前布局位置的 view。 + iOS 15 及以前(包括用 Xcode 13 编译的 App 运行在 iOS 16 上的场景),键盘的 UI 层级是: + |- UIApplication.windows + |- UIRemoteKeyboardWindow + |- UIInputSetContainerView + |- UIInputSetHostView - 键盘及 webView 里的输入工具栏(上下键、Done键) + |- _UIKBCompatInputView - 键盘主体按键 + |- TUISystemInputAssistantView - 键盘顶部的候选词栏、emoji 键盘顶部的搜索框 + |- _UIRemoteKeyboardPlaceholderView - webView 里的输入工具栏的占位(实际的 view 在 UITextEffectsWindow 里) + + iOS 16 及以后(仅限用 Xcode 14 及以上版本编译的 App),UIApplication.windows 里已经不存在 UIRemoteKeyboardWindow 了,所以退而求其次,我们通过 UITextEffectsWindow 里的 UIInputSetHostView 来获取键盘的位置——这两个 window 在布局层面可以理解为镜像关系。 + |- UIApplication.windows + |- UITextEffectsWindow + |- UIInputSetContainerView + |- UIInputSetHostView - 键盘及 webView 里的输入工具栏(上下键、Done键) + |- _UIRemoteKeyboardPlaceholderView - 整个键盘区域,包含顶部候选词栏、emoji 键盘顶部搜索栏(有时候不一定存在) + |- UIWebFormAccessory - webView 里的输入工具栏的占位 + |- TUIInputAssistantHostView - 外接键盘时可能存在,此时不一定有 placeholder + |- UIInputSetHostView - 可能存在多个,但只有一个里面有 _UIRemoteKeyboardPlaceholderView + + 所以只要找到 UIInputSetHostView 即可,优先从 UIRemoteKeyboardWindow 找,不存在的话则从 UITextEffectsWindow 找。 + */ + (UIView *)keyboardView { - for (UIWindow *window in [UIApplication sharedApplication].windows) { - UIView *view = [self getKeyboardViewFromWindow:window]; - if (view) { - return view; - } - } - return nil; -} - -+ (UIView *)getKeyboardViewFromWindow:(UIWindow *)window { + UIView *inputSetHostView = [[UIApplication.sharedApplication.windows qmui_filterWithBlock:^BOOL(__kindof UIWindow * _Nonnull window) { + return [NSStringFromClass(window.class) isEqualToString:@"UIRemoteKeyboardWindow"]; + }] qmui_compactMapWithBlock:^id _Nullable(__kindof UIWindow * _Nonnull window) { + return [self inputSetHostViewInWindow:window]; + }].firstObject; - if (!window) return nil; + if (inputSetHostView) return inputSetHostView; - NSString *windowName = NSStringFromClass(window.class); - if (![windowName isEqualToString:@"UIRemoteKeyboardWindow"]) { - return nil; - } + inputSetHostView = [[UIApplication.sharedApplication.windows qmui_filterWithBlock:^BOOL(__kindof UIWindow * _Nonnull window) { + return [NSStringFromClass(window.class) isEqualToString:@"UITextEffectsWindow"]; + }] qmui_compactMapWithBlock:^id _Nullable(__kindof UIWindow * _Nonnull window) { + return [self inputSetHostViewInWindow:window]; + }].firstObject; - for (UIView *view in window.subviews) { - NSString *viewName = NSStringFromClass(view.class); - if (![viewName isEqualToString:@"UIInputSetContainerView"]) { - continue; - } - for (UIView *subView in view.subviews) { - NSString *subViewName = NSStringFromClass(subView.class); - if (![subViewName isEqualToString:@"UIInputSetHostView"]) { - continue; - } - return subView; - } + return inputSetHostView; +} + ++ (UIView *)inputSetHostViewInWindow:(UIWindow *)window { + UIView *result = [[window.subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull subview) { + return [NSStringFromClass(subview.class) isEqualToString:@"UIInputSetContainerView"]; + }].subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull subview) { + return [NSStringFromClass(subview.class) isEqualToString:@"UIInputSetHostView"] && subview.subviews.count; + }]; + return result; +} + ++ (UIWindow *)keyboardWindow { + UIView *inputSetHostView = [self keyboardView]; + if (inputSetHostView) return inputSetHostView.window; + + UIWindow *window = [UIApplication.sharedApplication.windows qmui_firstMatchWithBlock:^BOOL(__kindof UIWindow * _Nonnull item) { + return [NSStringFromClass(item.class) isEqualToString:@"UIRemoteKeyboardWindow"]; + }]; + if (window) { + return window; } - return nil; + window = [UIApplication.sharedApplication.windows qmui_firstMatchWithBlock:^BOOL(__kindof UIWindow * _Nonnull item) { + return [NSStringFromClass(item.class) isEqualToString:@"UITextEffectsWindow"]; + }]; + return window; } + (BOOL)isKeyboardVisible { @@ -889,11 +926,21 @@ + (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 { - CGRect visibleRect = CGRectIntersection(CGRectFlatted(keyboardWindow.bounds), CGRectFlatted(keyboardView.frame)); + // 开启了系统的“设置→辅助功能→动态效果→减弱动态效果→首选交叉淡出过渡效果”后,键盘动画不再是 slide,而是 fade,此时应该用 alpha 来判断 + // https://github.com/Tencent/QMUI_iOS/issues/1173 + if (keyboardView.alpha <= 0) { + return 0; + } + + CGRect keyboardFrame = [keyboardWindow qmui_convertRect:keyboardView.bounds fromView:keyboardView]; + CGRect visibleRect = CGRectIntersection(keyboardWindow.bounds, keyboardFrame); if (CGRectIsValidated(visibleRect)) { return CGRectGetHeight(visibleRect); } diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILabel.h b/QMUI/QMUIKit/QMUIComponents/QMUILabel.h index 4c0108c0..4e5f0bac 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUILabel.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUILabel.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILabel.h @@ -15,6 +15,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + /** * `QMUILabel`支持通过`contentEdgeInsets`属性来实现类似padding的效果。 * @@ -25,17 +27,28 @@ /// 控制label内容的padding,默认为UIEdgeInsetsZero @property(nonatomic,assign) UIEdgeInsets contentEdgeInsets; +/// 支持在 label 无法显示完整文字时在 label 的末尾显示一个自定义的 View(通常用来实现点击展开更多的交互) +@property(nonatomic, strong, nullable) __kindof UIView *truncatingTailView; + /// 是否需要长按复制的功能,默认为 NO。 /// 长按时的背景色通过`highlightedBackgroundColor`设置。 @property(nonatomic,assign) IBInspectable BOOL canPerformCopyAction; /// 当 canPerformCopyAction 开启时,长按出来的菜单上的复制按钮的文本,默认为 nil,nil 时 menuItem 上的文字为“复制” -@property(nonatomic, copy) IBInspectable NSString *menuItemTitleForCopyAction; +@property(nonatomic, copy, nullable) IBInspectable NSString *menuItemTitleForCopyAction; -/// 如果打开了`canPerformCopyAction`,则长按时背景色将会被改为`highlightedBackgroundColor` -@property(nonatomic,strong) IBInspectable UIColor *highlightedBackgroundColor; +/** + label 在 highlighted 时的背景色,通常用于两种场景: + 1. 开启了 canPerformCopyAction 时,长按后的背景色 + 2. 作为 subviews 放在 UITableViewCell 上,当 cell highlighted 时,label 也会触发 highlighted,此时背景色也会显示为这个属性的值 + + 默认为 nil +*/ +@property(nonatomic,strong, nullable) IBInspectable UIColor *highlightedBackgroundColor UI_APPEARANCE_SELECTOR; /// 点击了“复制”后的回调 -@property(nonatomic, copy) void (^didCopyBlock)(QMUILabel *label, NSString *stringCopied); +@property(nonatomic, copy, nullable) void (^didCopyBlock)(QMUILabel *label, NSString *stringCopied); @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILabel.m b/QMUI/QMUIKit/QMUIComponents/QMUILabel.m index 6f423470..f50ddfad 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUILabel.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUILabel.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILabel.m @@ -15,6 +15,7 @@ #import "QMUILabel.h" #import "QMUICore.h" +#import "UILabel+QMUI.h" @interface QMUILabel () @@ -49,6 +50,33 @@ - (CGSize)intrinsicContentSize { return [self sizeThatFits:CGSizeMake(preferredMaxLayoutWidth, CGFLOAT_MAX)]; } +- (void)layoutSubviews { + [super layoutSubviews]; + if (self.truncatingTailView && self.attributedText.length) { + [self bringSubviewToFront:self.truncatingTailView]; + + // 不能通过修改 numberOfLines = 0 再恢复它的值,来计算高度是否折叠了,因为修改它的值会触发 layout,从而陷入死循环,所以这里只能通过 NSAttributedString 来计算内容的实际高度。注意如果 lineBreakMode 为 Tail 的话,NSAttributedString 必定只能计算单行的高度,所以要手动改为非 Tail 的值 + CGSize limitSize = CGSizeMake(CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets), CGFLOAT_MAX); + NSMutableAttributedString *string = self.attributedText.mutableCopy; + if (self.numberOfLines != 1 && self.lineBreakMode == NSLineBreakByTruncatingTail) { + NSParagraphStyle *p = [string attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:nil]; + if (p) { + NSMutableParagraphStyle *mutableP = p.mutableCopy; + mutableP.lineBreakMode = NSLineBreakByWordWrapping; + [string addAttribute:NSParagraphStyleAttributeName value:mutableP range:NSMakeRange(0, string.length)]; + } + } + CGSize realSize = [string boundingRectWithSize:limitSize options:NSStringDrawingUsesLineFragmentOrigin context:nil].size; + BOOL shouldShowTruncatingTailView = realSize.height > CGRectGetHeight(self.bounds); + self.truncatingTailView.hidden = !shouldShowTruncatingTailView; + if (!self.truncatingTailView.hidden) { + CGFloat lineHeight = self.qmui_lineHeight; + [self.truncatingTailView sizeToFit]; + self.truncatingTailView.frame = CGRectMake(CGRectGetWidth(self.bounds) - self.contentEdgeInsets.right - CGRectGetWidth(self.truncatingTailView.frame), CGRectGetHeight(self.bounds) - self.contentEdgeInsets.bottom - lineHeight, CGRectGetWidth(self.truncatingTailView.frame), lineHeight); + } + } +} + - (void)drawTextInRect:(CGRect)rect { rect = UIEdgeInsetsInsetRect(rect, self.contentEdgeInsets); @@ -61,19 +89,39 @@ - (void)drawTextInRect:(CGRect)rect { [super drawTextInRect:rect]; } -- (void)setHighlightedBackgroundColor:(UIColor *)highlightedBackgroundColor { - _highlightedBackgroundColor = highlightedBackgroundColor; +- (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; - if (highlightedBackgroundColor) { - self.originalBackgroundColor = self.backgroundColor; + if (self.highlightedBackgroundColor) { + [super setBackgroundColor:highlighted ? self.highlightedBackgroundColor : self.originalBackgroundColor]; } } -- (void)setHighlighted:(BOOL)highlighted { - [super setHighlighted:highlighted]; +- (void)setBackgroundColor:(UIColor *)backgroundColor { + self.originalBackgroundColor = backgroundColor; - if (self.highlightedBackgroundColor) { - self.backgroundColor = highlighted ? self.highlightedBackgroundColor : self.originalBackgroundColor; + // 在出现 menu 的时候 backgroundColor 被修改,此时也不应该立马显示新的 backgroundColor + if (self.highlighted && self.highlightedBackgroundColor) { + return; + } + + [super setBackgroundColor:backgroundColor]; +} + +// 当 label.highlighted = YES 时 backgroundColor 的 getter 会返回 self.highlightedBackgroundColor,因此如果在 highlighted = YES 时外部刚好执行了 `label.backgroundColor = label.backgroundColor` 就会导致 label 的背景色被错误地设置为高亮时的背景色,所以这里需要重写 getter 返回内部记录的 originalBackgroundColor +- (UIColor *)backgroundColor { + return self.originalBackgroundColor; +} + +#pragma mark - 自定义缩略点点点按钮 + +- (void)setTruncatingTailView:(__kindof UIView *)truncatingTailView { + if (_truncatingTailView != truncatingTailView) { + [_truncatingTailView removeFromSuperview]; + _truncatingTailView = truncatingTailView; + [self addSubview:_truncatingTailView]; + _truncatingTailView.hidden = YES; + [self setNeedsLayout]; } } @@ -87,10 +135,6 @@ - (void)setCanPerformCopyAction:(BOOL)canPerformCopyAction { [self addGestureRecognizer:self.longGestureRecognizer]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMenuWillHideNotification:) name:UIMenuControllerWillHideMenuNotification object:nil]; - - if (!self.highlightedBackgroundColor) { - self.highlightedBackgroundColor = TableViewCellSelectedBackgroundColor; // 设置个默认值 - } } else if (!_canPerformCopyAction && self.longGestureRecognizer) { [self removeGestureRecognizer:self.longGestureRecognizer]; self.longGestureRecognizer = nil; @@ -133,12 +177,11 @@ - (void)handleLongPressGestureRecognizer:(UIGestureRecognizer *)gestureRecognize UIMenuController *menuController = [UIMenuController sharedMenuController]; UIMenuItem *copyMenuItem = [[UIMenuItem alloc] initWithTitle:self.menuItemTitleForCopyAction ?: @"复制" action:@selector(copyString:)]; [[UIMenuController sharedMenuController] setMenuItems:@[copyMenuItem]]; - [menuController setTargetRect:self.frame inView:self.superview]; - [menuController setMenuVisible:YES animated:YES]; + [menuController showMenuFromView:self.superview rect:self.frame]; - [self setHighlighted:YES]; + self.highlighted = YES; } else if (gestureRecognizer.state == UIGestureRecognizerStatePossible) { - [self setHighlighted:NO]; + self.highlighted = NO; } } diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouter.h b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouter.h new file mode 100644 index 00000000..391cbc8f --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouter.h @@ -0,0 +1,19 @@ +/** + * 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. + */ + +// +// QMUILayouter.h +// QMUIKit +// +// Created by QMUI Team on 2024/1/2. +// + +#import +#import +#import "QMUILayouterLinearHorizontal.h" +#import "QMUILayouterLinearVertical.h" diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterItem.h b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterItem.h new file mode 100644 index 00000000..7323fd28 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterItem.h @@ -0,0 +1,135 @@ +/** + * 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. + */ + +// +// QMUILayouterItem.h +// QMUIKit +// +// Created by QMUI Team on 2024/1/2. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, QMUILayouterAlignment) { + /// 对水平容器来说是从左往右,对竖直容器来说是从上往下。若容器大小不足以容纳所有 item,则末尾的 item 大小会被强制裁剪以保证不溢出。 + QMUILayouterAlignmentLeading, + + /// 对水平容器来说是从左往右然后整体右对齐父容器,对竖直容器来说是从上往下然后整体底对齐父容器。若 item 超过父容器大小,则与 QMUILayouterAlignmentLeading 一致。 + QMUILayouterAlignmentTrailing, + + /// 对水平容器来说是从左往右然后整体在父容器里居中,对竖直容器来说是从上往下然后整体在父容器里居中。若 item 超过父容器大小,则与 QMUILayouterAlignmentLeading 一致。 + QMUILayouterAlignmentCenter, + + /// 当表示与容器布局方向相同的方向时(例如 Linear 的水平,或 Vertical 的竖直),仅当子元素个数为1时有效,会在指定方向上撑满父容器。当子元素个数大于1时与 QMUILayouterAlignmentLeading 一致。 + /// 当表示与容器布局方向垂直的方向时(例如 Linear 的竖直,或 Vertical 的水平),则所有子元素均会在指定方向上撑满父容器。 + QMUILayouterAlignmentFill, +}; + +/// 表示父容器还有剩余空间时当前 item 也保持自身尺寸不变,不去拉伸填充剩余空间 +extern const CGFloat QMUILayouterGrowNever; + +/// 表示父容器还有剩余空间时当前 item 以最高优先级去填充(一般用1就行,不需要用到 Most) +extern const CGFloat QMUILayouterGrowMost; + +/// 表示父容器空间不足以容纳所有 item,不得已要压缩 item 时,不要压缩当前 item +extern const CGFloat QMUILayouterShrinkNever; + +/// 表示父容器空间不足以容纳所有 item,不得已要压缩 item 时,允许压缩当前 item(按各自尺寸比例) +extern const CGFloat QMUILayouterShrinkDefault; + +/// 表示父容器空间不足以容纳所有 item,不得已要压缩 item 时,使当前 item 以最高优先级压缩 +extern const CGFloat QMUILayouterShrinkMost; + +@interface QMUILayouterItem : NSObject + +/// 通常用于生成一个子元素角色的 item,不允许拉伸也不允许缩放。 ++ (instancetype)itemWithView:(__kindof UIView *)view margin:(UIEdgeInsets)margin; + +/// 通常用于生成一个子元素角色的 item ++ (instancetype)itemWithView:(__kindof UIView *)view margin:(UIEdgeInsets)margin grow:(CGFloat)grow shrink:(CGFloat)shrink; + +/// 关联的实体 view,如果当前 item 是虚拟布局容器,也可以不存在关联的实体 view。 +/// @note 一般将 view 添加到界面上后再赋值给这个属性,这样可确保后续的运算最准确。 +@property(nonatomic, weak, nullable) __kindof UIView *view; + +/// frame 的值变化时才会设置给 view 且标记为在下一次 runloop 里需要刷新布局。 +@property(nonatomic, assign) CGRect frame; + +/// 给 parentItem 布局自己时使用,自己内部 layout 时不使用,也不包含在自身的 sizeThatFits: 结果里。 +@property(nonatomic, assign) UIEdgeInsets margin; + +/// 表示父容器在布局自己时可忽略 item 自身的宽度,仅通过将所有 grow 大于0的 item 按各自 grow 比例计算得到宽度,例如一行里有两个 item,一个 item 宽度为自身内容宽度,另一个 item 撑满容器剩余空间。默认为 QMUILayouterGrowNever,也即自适应内容,设置为 QMUILayouterGrowMost 或某个大于0的数值可按比例撑满容器。 +/// @warning 仅在支持比例布局的容器里有效(例如 LinearHorizontal、LinearVertical) +@property(nonatomic, assign) CGFloat grow; + +/// 当父容器空间不足以容纳所有 item 时,由每个 item 的 shrink 值及 item 的尺寸来决定该压缩哪个 item 的尺寸、压缩多少。默认为 QMUILayouterShrinkNever,值越大则压缩得越狠。 +@property(nonatomic, assign) CGFloat shrink; + +/// 最大的尺寸,在自身 sizeThatFits、父容器 grow 时生效,在 setFrame 时不限制(也即非要的话你也可以设置一个突破限制的尺寸),默认为 CGSizeMax +@property(nonatomic, assign) CGSize maximumSize; + +/// 最小的尺寸,在自身 sizeThatFits、父容器 shrink 时生效,在 setFrame 时不限制(也即非要的话你也可以设置一个突破限制的尺寸),默认为 CGSizeZero +@property(nonatomic, assign) CGSize minimumSize; + +/// 当前 item 是否可视,仅可视的 item 会参与布局运算。 +@property(nonatomic, assign, readonly) BOOL visible; + +/// 允许业务自定义 visible 的逻辑。 +@property(nonatomic, copy, nullable) BOOL (^visibleBlock)(QMUILayouterItem *aItem); + +/// 父容器,在 setChildItems: 时会将父子关系关联起来。 +@property(nonatomic, weak, readonly, nullable) __kindof QMUILayouterItem *parentItem; + +/// 所有子元素 +@property(nonatomic, strong) NSArray *childItems; + +/// 所有 visible 为 YES 的子元素,布局运算时使用这个。 +@property(nonatomic, weak, readonly, nullable) NSArray *visibleChildItems; + +// 便捷方法,会自动判空 +@property(nonatomic, weak, readonly, nullable) QMUILayouterItem *visibleChildItem0; +@property(nonatomic, weak, readonly, nullable) QMUILayouterItem *visibleChildItem1; +@property(nonatomic, weak, readonly, nullable) QMUILayouterItem *visibleChildItem2; +@property(nonatomic, weak, readonly, nullable) QMUILayouterItem *visibleChildItem3; + +/// 计算在特定宽高下的自身尺寸,注意 self.margin 不参与其中。通常将 height 传 CGFLOAT_MAX 以得到一个自适应内容的大小。 +- (CGSize)sizeThatFits:(CGSize)size; + +/// 允许业务自定义 sizeThatFits: 的逻辑(注意这个主要用于父容器布局时询问子元素大小用,不用于元素计算自身内容大小时用),在调用完 block 后才进行 min/height 保护。 +@property(nonatomic, copy, nullable) CGSize (^sizeThatFitsBlock)(QMUILayouterItem *aItem, CGSize size, CGSize superResult); + +/// 保持 x/y 不变,将自身大小设置为不受宽高限制的尺寸,并将布局标记为需要被刷新。 +- (void)sizeToFit; + +/// 标记需要刷新布局,在同一个 runloop 里的所有 setNeedsLayout 会统一在下一个 runloop 里才一起布局。 +- (void)setNeedsLayout; + +/// 如果当前布局待刷新,则立即刷新,以便得到最新的布局结果。 +- (void)layoutIfNeeded; + +/// 是否在指定 view 的坐标系里显示自身及所有子元素的布局边框(颜色随机),请在 layoutSubviews、viewDidLayoutSubviews 里调用(也即每次参数 view 的布局发生变化时)。 +- (void)showDebugBorderRecursivelyInView:(UIView *)view; + +/// 一般用作调试时区分用,业务随意赋值。 +@property(nonatomic, copy, nullable) NSString *identifier; +@end + +@interface QMUILayouterItem (UISubclassingHooks) + +/// 子类计算自身大小的逻辑请写在这个方法里,如果是外部希望得知当前元素的大小,请调用 sizeThatFits: 或 sizeToFit。 +/// @param shouldConsiderBlock 计算大小时是否需要考虑 sizeThatFitsBlock:,如果当前是外部询问元素大小,参数为 YES,如果是内部希望得知内容实际大小,参数为 NO。 +- (CGSize)sizeThatFits:(CGSize)size shouldConsiderBlock:(BOOL)shouldConsiderBlock; + +/// 子类重写布局时使用,外部不要直接调用它。可视情况自行决定是否要调用 super。 +- (void)layout; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterItem.m b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterItem.m new file mode 100644 index 00000000..9dea640a --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterItem.m @@ -0,0 +1,286 @@ +/** + * 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. + */ + +// +// QMUILayouterItem.m +// QMUIKit +// +// Created by QMUI Team on 2024/1/2. +// + +#import "QMUILayouterItem.h" +#import "QMUICore.h" +#import "NSArray+QMUI.h" +#import "NSString+QMUI.h" +#import "CALayer+QMUI.h" +#import "UIColor+QMUI.h" + +const CGFloat QMUILayouterGrowNever = 0.0; +const CGFloat QMUILayouterGrowMost = 99.0; +const CGFloat QMUILayouterShrinkDefault = 1.0; +const CGFloat QMUILayouterShrinkNever = 0.0; +const CGFloat QMUILayouterShrinkMost = 99.0; + +@interface QMUILayouterItem () +@property(nonatomic, strong) CALayer *debugBorderLayer; +@end + +@implementation QMUILayouterItem { + BOOL _shouldInvalidateLayout; +} + ++ (instancetype)itemWithView:(__kindof UIView *)view margin:(UIEdgeInsets)margin { + return [self itemWithView:view margin:margin grow:QMUILayouterGrowNever shrink:QMUILayouterShrinkNever]; +} + ++ (instancetype)itemWithView:(__kindof UIView *)view margin:(UIEdgeInsets)margin grow:(CGFloat)grow shrink:(CGFloat)shrink { + QMUILayouterItem *item = [[self alloc] init]; + item.view = view; + item.margin = margin; + item.grow = grow; + item.shrink = shrink; + return item; +} + +- (instancetype)init { + if (self = [super init]) { + _maximumSize = CGSizeMax; + _minimumSize = CGSizeZero; + } + return self; +} + +- (NSString *)description { + NSString * (^growName)(CGFloat grow) = ^NSString * (CGFloat grow) { + if (grow == QMUILayouterGrowNever) return @"Never"; + if (grow == QMUILayouterGrowMost) return @"Most"; + return [NSString stringWithFormat:@"%.1f", grow]; + }; + NSString * (^shrinkName)(CGFloat shrink) = ^NSString * (CGFloat shrink) { + if (shrink == QMUILayouterShrinkDefault) return @"Default"; + if (shrink == QMUILayouterShrinkNever) return @"Never"; + if (shrink == QMUILayouterShrinkMost) return @"Most"; + return [NSString stringWithFormat:@"%.1f", shrink]; + }; + return [NSString qmui_stringByConcat:[super description], @", visible = ", StringFromBOOL(self.visible), @", frame = ", NSStringFromCGRect(self.frame), @", margin = ", NSStringFromUIEdgeInsets(self.margin), @", grow = ", growName(self.grow), @", shrink = ", shrinkName(self.shrink), (self.visibleChildItems.count ? [NSString stringWithFormat:@", visibleChild(%@)", @(self.visibleChildItems.count)] : @""), (self.view ? [NSString stringWithFormat:@", view = <%@: %p>", NSStringFromClass(self.view.class), self.view] : @""), nil]; +} + +@synthesize frame = _frame; +- (void)setFrame:(CGRect)frame { + // QMUIViewSelfSizingHeight 的功能 + if (isinf(frame.size.height)) { + if (frame.size.width > 0) { + CGFloat height = flat([self sizeThatFits:CGSizeMake(CGRectGetWidth(frame), CGFLOAT_MAX) shouldConsiderBlock:NO].height); + frame = CGRectSetHeight(frame, height); + } else { + frame.size.height = _frame.size.height; + } + } + BOOL frameChanged = !CGRectEqualToRect(self.frame, frame); + _frame = frame; + self.view.frame = frame; + if (frameChanged) { + [self setNeedsLayout]; + } +} + +- (CGRect)frame { + // 每个 item 不一定都存在 view,可能它只是一个虚拟的布局节点,所以这里要区分 + if (self.view) { + return self.view.frame; + } + return _frame; +} + +- (void)setView:(__kindof UIView *)view { + BOOL valueChanged = _view != view; + _view = view; + if (valueChanged) { + [self setNeedsLayout]; + } +} + +- (void)setMargin:(UIEdgeInsets)margin { + BOOL valueChanged = UIEdgeInsetsEqualToEdgeInsets(_margin, margin); + _margin = margin; + if (valueChanged) { + [self.parentItem setNeedsLayout]; + } +} + +- (void)setGrow:(CGFloat)grow { + NSAssert(grow >= 0, @"negative values are invalid for grow."); + grow = MAX(0, grow); + BOOL valueChanged = _grow != grow; + _grow = grow; + if (valueChanged) { + [self.parentItem setNeedsLayout]; + } +} + +- (void)setShrink:(CGFloat)shrink { + NSAssert(shrink >= 0, @"negative values are invalid for grow."); + shrink = MAX(0, shrink); + BOOL valueChanged = _shrink != shrink; + _shrink = shrink; + if (valueChanged) { + [self.parentItem setNeedsLayout]; + } +} + +- (BOOL)visible { + if (self.visibleBlock) return self.visibleBlock(self); + return self.view.superview && !self.view.hidden; +} + +- (QMUILayouterItem *)visibleChildItem0 { + return [self visibleChildItemAtIndex:0]; +} + +- (QMUILayouterItem *)visibleChildItem1 { + return [self visibleChildItemAtIndex:1]; +} + +- (QMUILayouterItem *)visibleChildItem2 { + return [self visibleChildItemAtIndex:2]; +} + +- (QMUILayouterItem *)visibleChildItem3 { + return [self visibleChildItemAtIndex:3]; +} + +- (QMUILayouterItem *)visibleChildItemAtIndex:(NSUInteger)index { + return index < self.visibleChildItems.count ? self.visibleChildItems[index] : nil; +} + +- (void)setChildItems:(NSArray *)childItems { + [_childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + obj->_parentItem = nil; + }]; + _childItems = childItems; + [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + obj->_parentItem = self; + }]; +} + +- (NSArray *)visibleChildItems { + return self.childItems.count ? [self.childItems qmui_filterWithBlock:^BOOL(QMUILayouterItem * _Nonnull item) { + return item.visible; + }] : nil; +} + +- (CGSize)sizeThatFits:(CGSize)size { + return [self sizeThatFits:size shouldConsiderBlock:YES]; +} + +- (void)sizeToFit { + CGSize prefersSize = CGSizeMax; + // 参照系统 UILabel 的 sizeToFit 方式(在当前宽度下计算高度) + if ([self.view isKindOfClass:UILabel.class] && CGRectGetWidth(self.frame) > 0) { + prefersSize.width = CGRectGetWidth(self.frame); + } + CGSize size = [self sizeThatFits:prefersSize]; + self.frame = CGRectSetSize(self.frame, size); +} + +- (void)setNeedsLayout { + if (_shouldInvalidateLayout) return; + _shouldInvalidateLayout = YES; + dispatch_async(dispatch_get_main_queue(), ^{ + if (self->_shouldInvalidateLayout) { + [self layoutIfNeeded]; + } + }); +} + +- (void)layoutIfNeeded { + [self layout]; + [self layoutDebugBorderLayer]; + _shouldInvalidateLayout = NO; +} + +- (CALayer *)generateDebugBorderLayerContainer { + CALayer *layer = CALayer.layer; + layer.name = @"QMUILayouterDebugBorderLayerContainer"; + [layer qmui_removeDefaultAnimations]; + return layer; +} + +- (CALayer *)generateDebugBorderLayer { + CALayer *layer = CALayer.layer; + layer.name = @"QMUILayouterDebugBorderLayer"; + [layer qmui_removeDefaultAnimations]; + UIColor *color = UIColor.qmui_randomColor; + layer.backgroundColor = [color colorWithAlphaComponent:.1].CGColor; + layer.borderColor = color.CGColor; + layer.borderWidth = 1; + return layer; +} + +- (void)showDebugBorderRecursivelyInView:(UIView *)view { + if (!view) return; + CALayer *container = [view.layer.sublayers qmui_firstMatchWithBlock:^BOOL(__kindof CALayer * _Nonnull item) { + return [item.name isEqualToString:@"QMUILayouterDebugBorderLayerContainer"]; + }]; + if (!container) { + container = [self generateDebugBorderLayerContainer]; + [view.layer addSublayer:container]; + } + [container.sublayers.copy enumerateObjectsUsingBlock:^(__kindof CALayer * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if ([obj.name isEqualToString:@"QMUILayouterDebugBorderLayer"]) [obj removeFromSuperlayer]; + }]; + container.frame = view.bounds; + [self showDebugBorderInContainer:container]; + [self.childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [obj showDebugBorderInContainer:container]; + }]; +} + +- (void)showDebugBorderInContainer:(CALayer *)container { + if (!container) return; + if (!self.debugBorderLayer) { + self.debugBorderLayer = [self generateDebugBorderLayer]; + [container addSublayer:self.debugBorderLayer]; + } else if (self.debugBorderLayer.superlayer != container) { + [self.debugBorderLayer removeFromSuperlayer]; + [container addSublayer:self.debugBorderLayer]; + } +} + +- (void)layoutDebugBorderLayer { + if (!self.debugBorderLayer || !self.debugBorderLayer.superlayer) return; + if (self.view) { + UIView *containerView = (UIView *)self.debugBorderLayer.superlayer.superlayer.delegate; + CGRect frame = [self.view convertRect:self.view.bounds toView:containerView]; + self.debugBorderLayer.frame = frame; + } else { + self.debugBorderLayer.frame = self.frame; + } +} + +@end + +@implementation QMUILayouterItem (UISubclassingHooks) + +- (void)layout { +} + +- (CGSize)sizeThatFits:(CGSize)size shouldConsiderBlock:(BOOL)shouldConsiderBlock { + if (CGSizeEqualToSize(self.view.bounds.size, size) || CGSizeIsEmpty(size)) { + size = CGSizeMax; + } + CGSize result = [self.view sizeThatFits:size]; + if (shouldConsiderBlock && self.sizeThatFitsBlock) { + result = self.sizeThatFitsBlock(self, size, result); + } + result.width = MIN(self.maximumSize.width, MAX(self.minimumSize.width, result.width)); + result.height = MIN(self.maximumSize.height, MAX(self.minimumSize.height, result.height)); + return result; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearHorizontal.h b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearHorizontal.h new file mode 100644 index 00000000..98e3655d --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearHorizontal.h @@ -0,0 +1,46 @@ +/** + * 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. + */ + +// +// QMUILayouterLinearHorizontal.h +// QMUIKit +// +// Created by QMUI Team on 2024/1/2. +// + +#import +#import +#import "QMUILayouterItem.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + 水平方向的线性布局,若容器大小不足以容纳所有 item,则末尾的 item 大小会被强制裁剪以保证不溢出。 + 子元素可通过设置自己的 grow 来达到撑满容器的效果。 + */ +@interface QMUILayouterLinearHorizontal : QMUILayouterItem + ++ (instancetype)itemWithChildItems:(NSArray *)childItems + spacingBetweenItems:(CGFloat)spacingBetweenItems; + ++ (instancetype)itemWithChildItems:(NSArray *)childItems + spacingBetweenItems:(CGFloat)spacingBetweenItems + horizontal:(QMUILayouterAlignment)horizontal + vertical:(QMUILayouterAlignment)vertical; + +/// 子元素之间的间距 +@property(nonatomic, assign) CGFloat spacingBetweenItems; + +/// 子元素水平方向上的布局方式,默认为 QMUILayouterAlignmentLeading,每种 enum 的布局说明请查看 enum 定义。 +@property(nonatomic, assign) QMUILayouterAlignment childHorizontalAlignment; + +/// 子元素竖直方向上的布局方式,默认为 QMUILayouterAlignmentLeading,每种 enum 的布局说明请查看 enum 定义。 +@property(nonatomic, assign) QMUILayouterAlignment childVerticalAlignment; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearHorizontal.m b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearHorizontal.m new file mode 100644 index 00000000..ecedf972 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearHorizontal.m @@ -0,0 +1,189 @@ +/** + * 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. + */ + +// +// QMUILayouterLinearHorizontal.m +// QMUIKit +// +// Created by QMUI Team on 2024/1/2. +// + +#import "QMUILayouterLinearHorizontal.h" +#import "QMUICore.h" +#import "NSArray+QMUI.h" +#import "UIView+QMUI.h" + +@implementation QMUILayouterLinearHorizontal + ++ (instancetype)itemWithChildItems:(NSArray *)childItems spacingBetweenItems:(CGFloat)spacingBetweenItems { + return [self itemWithChildItems:childItems spacingBetweenItems:spacingBetweenItems horizontal:QMUILayouterAlignmentLeading vertical:QMUILayouterAlignmentLeading]; +} + ++ (instancetype)itemWithChildItems:(NSArray *)childItems spacingBetweenItems:(CGFloat)spacingBetweenItems horizontal:(QMUILayouterAlignment)horizontal vertical:(QMUILayouterAlignment)vertical { + QMUILayouterLinearHorizontal *item = [[self alloc] init]; + item.childItems = childItems; + item.spacingBetweenItems = spacingBetweenItems; + item.childHorizontalAlignment = horizontal; + item.childVerticalAlignment = vertical; + return item; +} + +- (NSString *)description { + NSString * (^alignmentName)(QMUILayouterAlignment alignment) = ^NSString *(QMUILayouterAlignment alignment) { + return @[@"Leading", @"Trailing", @"Center", @"Fill"][alignment]; + }; + return [NSString qmui_stringByConcat:[super description], @", horizontal = ", alignmentName(self.childHorizontalAlignment), @", vertical = ", alignmentName(self.childVerticalAlignment), nil]; +} + +// 容器性质的 layouter,不存在关联的实体 view,则始终认为是可视的,如果是 parentItem 的 parentItem 不可见,则由 parentItem 自己去管 +- (BOOL)visible { + if (self.visibleBlock) return self.visibleBlock(self); + return self.visibleChildItems.count; +} + +- (CGSize)sizeThatFits:(CGSize)size shouldConsiderBlock:(BOOL)shouldConsiderBlock { + NSArray *childItems = self.visibleChildItems; + if (!childItems.count) return self.minimumSize; + if (CGSizeEqualToSize(self.frame.size, size) || CGSizeIsEmpty(size)) { + size = CGSizeMax; + } + __block CGSize contentSize = CGSizeZero; + __block CGFloat totalShrink = QMUILayouterShrinkNever; + __block NSMutableDictionary *cachedSize = NSMutableDictionary.new; + [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + CGSize s = [obj sizeThatFits:CGSizeMax]; + cachedSize[[NSString stringWithFormat:@"%p", obj]] = [NSValue valueWithCGSize:s]; + contentSize.width += s.width + UIEdgeInsetsGetHorizontalValue(obj.margin) + self.spacingBetweenItems; + contentSize.height = MAX(contentSize.height, s.height + UIEdgeInsetsGetVerticalValue(obj.margin)); + + if (obj.shrink > QMUILayouterShrinkNever) { + totalShrink += s.width * obj.shrink; + } + }]; + contentSize.width -= self.spacingBetweenItems; + if (contentSize.width <= size.width || totalShrink == QMUILayouterShrinkNever) { + if (shouldConsiderBlock && self.sizeThatFitsBlock) { + contentSize = self.sizeThatFitsBlock(self, size, contentSize); + } + contentSize.width = MIN(self.maximumSize.width, MAX(self.minimumSize.width, contentSize.width)); + contentSize.height = MIN(self.maximumSize.height, MAX(self.minimumSize.height, contentSize.height)); + return contentSize; + } + + __block CGSize resultSize = CGSizeZero; + [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + CGSize s = cachedSize[[NSString stringWithFormat:@"%p", obj]].CGSizeValue; + if (obj.shrink > QMUILayouterGrowNever) { + CGFloat spaceToShrink = contentSize.width - size.width; + CGFloat w = s.width - spaceToShrink * s.width * obj.shrink / totalShrink; + CGFloat h = [obj sizeThatFits:CGSizeMake(w, CGFLOAT_MAX)].height; + s.width = w; + s.height = h; + } + resultSize.width += s.width + UIEdgeInsetsGetHorizontalValue(obj.margin) + self.spacingBetweenItems; + resultSize.height = MAX(resultSize.height, s.height + UIEdgeInsetsGetVerticalValue(obj.margin)); + }]; + resultSize.width -= self.spacingBetweenItems; + if (shouldConsiderBlock && self.sizeThatFitsBlock) { + resultSize = self.sizeThatFitsBlock(self, size, resultSize); + } + resultSize.width = MIN(self.maximumSize.width, MAX(self.minimumSize.width, resultSize.width)); + resultSize.height = MIN(self.maximumSize.height, MAX(self.minimumSize.height, resultSize.height)); + return resultSize; +} + +- (void)layout { + NSArray *childItems = self.visibleChildItems; + CGSize contentSize = [self sizeThatFits:CGSizeMax shouldConsiderBlock:NO]; + + __block CGFloat totalGrow = QMUILayouterGrowNever; + __block CGFloat spaceToGrow = CGRectGetWidth(self.frame);// 父容器里待填充的多余空间(容器总大小减去所有固定的值,包括 spacingBetweenItems、所有 item 的 margin 区域、grow = Never 的 item 的 width之后,剩下的空间) + __block CGFloat totalShrink = QMUILayouterShrinkNever; + [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [obj sizeToFit]; + + spaceToGrow -= CGRectGetWidth(obj.frame) + UIEdgeInsetsGetHorizontalValue(obj.margin) + self.spacingBetweenItems; + if (obj.grow > QMUILayouterGrowNever) { + totalGrow += obj.grow; + } + + if (obj.shrink > QMUILayouterShrinkNever) { + totalShrink += CGRectGetWidth(obj.frame) * obj.shrink; + } + }]; + spaceToGrow += self.spacingBetweenItems; + BOOL shouldCalcGrow = totalGrow > QMUILayouterGrowNever && contentSize.width < CGRectGetWidth(self.frame); + BOOL shouldCalcShrink = totalShrink > QMUILayouterShrinkNever && contentSize.width > CGRectGetWidth(self.frame); + + __block CGFloat minX = CGRectGetMinX(self.frame); + __block CGFloat minY = CGRectGetMinY(self.frame); + __block CGFloat maxX = CGRectGetMaxX(self.frame); + __block CGFloat maxY = CGRectGetMaxY(self.frame); + QMUILayouterAlignment childHorizontalAlignment = self.childHorizontalAlignment; + QMUILayouterAlignment childVerticalAlignment = self.childVerticalAlignment; + + // 不需要考虑 grow/shrink 的情况,先把 minX 算出来 + if (!shouldCalcGrow && !shouldCalcShrink && childHorizontalAlignment != QMUILayouterAlignmentLeading) { + if (contentSize.width >= CGRectGetWidth(self.frame)) { + // 不管哪种布局方式,只要内容超过容器,统一按 Leading 处理 + childHorizontalAlignment = QMUILayouterAlignmentLeading; + } else if (childHorizontalAlignment == QMUILayouterAlignmentTrailing) { + minX = MAX(minX, CGRectGetMaxX(self.frame) - contentSize.width); + childHorizontalAlignment = QMUILayouterAlignmentLeading; + } else if (childHorizontalAlignment == QMUILayouterAlignmentCenter) { + minX = MAX(minX, minX + CGFloatGetCenter(CGRectGetWidth(self.frame), contentSize.width)); + childHorizontalAlignment = QMUILayouterAlignmentLeading; + } else if (childHorizontalAlignment == QMUILayouterAlignmentFill) { + if (childItems.count > 1) { + // 与容器相同方向的 Fill 仅在只有一个子元素时有效,超过一个子元素则视为 Leading + // 如果你希望多个 childItem 可拉伸铺满,应该用 childItem.grow 来控制,而不是用 Fill + childHorizontalAlignment = QMUILayouterAlignmentLeading; + } else { + // 一个子元素的情况,直接布局掉算了 + QMUILayouterItem *obj = self.visibleChildItem0; + obj.frame = CGRectSetX(obj.frame, minX + obj.margin.left); + obj.frame = CGRectSetWidth(obj.frame, maxX - obj.margin.right - CGRectGetMinX(obj.frame)); + } + } + } + + [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + obj.frame = CGRectSetX(obj.frame, minX + obj.margin.left); + if (shouldCalcGrow && obj.grow > QMUILayouterGrowNever) { + CGFloat w = CGRectGetWidth(obj.frame) + spaceToGrow * obj.grow / totalGrow; + obj.frame = CGRectSetSize(obj.frame, CGSizeMake(w, QMUIViewSelfSizingHeight)); + } + if (shouldCalcShrink && obj.shrink > QMUILayouterGrowNever) { + CGFloat spaceToShrink = contentSize.width - CGRectGetWidth(self.frame); + CGFloat w = CGRectGetWidth(obj.frame) - spaceToShrink * CGRectGetWidth(obj.frame) * obj.shrink / totalShrink; + w = MAX(0, w); + obj.frame = CGRectSetSize(obj.frame, CGSizeMake(w, QMUIViewSelfSizingHeight)); + obj.frame = CGRectSetHeight(obj.frame, MIN(CGRectGetHeight(self.frame) - UIEdgeInsetsGetVerticalValue(obj.margin), CGRectGetHeight(obj.frame))); + } + if (CGRectGetMaxX(obj.frame) + obj.margin.right > maxX) { + obj.frame = CGRectSetWidth(obj.frame, maxX - obj.margin.right - CGRectGetMinX(obj.frame)); + } + + minX = CGRectGetMaxX(obj.frame) + obj.margin.right + self.spacingBetweenItems; + + if (childVerticalAlignment == QMUILayouterAlignmentTrailing) { + obj.frame = CGRectSetY(obj.frame, maxY - obj.margin.bottom - CGRectGetHeight(obj.frame)); + } else if (childVerticalAlignment == QMUILayouterAlignmentCenter) { + obj.frame = CGRectSetY(obj.frame, minY + obj.margin.top + CGFloatGetCenter(maxY - minY - UIEdgeInsetsGetVerticalValue(obj.margin), CGRectGetHeight(obj.frame))); + } else if (childVerticalAlignment == QMUILayouterAlignmentFill) { + obj.frame = CGRectSetY(obj.frame, minY + obj.margin.top); + obj.frame = CGRectSetHeight(obj.frame, maxY - obj.margin.bottom - CGRectGetMinY(obj.frame)); + } else { + obj.frame = CGRectSetY(obj.frame, minY + obj.margin.top); + } + + [obj layoutIfNeeded]; + }]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearVertical.h b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearVertical.h new file mode 100644 index 00000000..95ab8e70 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearVertical.h @@ -0,0 +1,46 @@ +/** + * 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. + */ + +// +// QMUILayouterLinearVertical.h +// QMUIKit +// +// Created by QMUI Team on 2024/1/2. +// + +#import +#import +#import "QMUILayouterItem.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + 竖直方向的线性布局,若容器大小不足以容纳所有 item,则末尾的 item 大小会被强制裁剪以保证不溢出。 + 子元素可通过设置自己的 grow 来达到撑满容器的效果。 + */ +@interface QMUILayouterLinearVertical : QMUILayouterItem + ++ (instancetype)itemWithChildItems:(NSArray *)childItems + spacingBetweenItems:(CGFloat)spacingBetweenItems; + ++ (instancetype)itemWithChildItems:(NSArray *)childItems + spacingBetweenItems:(CGFloat)spacingBetweenItems + horizontal:(QMUILayouterAlignment)horizontal + vertical:(QMUILayouterAlignment)vertical; + +/// 子元素之间的间距 +@property(nonatomic, assign) CGFloat spacingBetweenItems; + +/// 子元素水平方向上的布局方式,默认为 QMUILayouterAlignmentLeading,每种 enum 的布局说明请查看 enum 定义。 +@property(nonatomic, assign) QMUILayouterAlignment childHorizontalAlignment; + +/// 子元素竖直方向上的布局方式,默认为 QMUILayouterAlignmentLeading,每种 enum 的布局说明请查看 enum 定义。 +@property(nonatomic, assign) QMUILayouterAlignment childVerticalAlignment; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearVertical.m b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearVertical.m new file mode 100644 index 00000000..5e66320d --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearVertical.m @@ -0,0 +1,187 @@ +/** + * 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. + */ + +// +// QMUILayouterLinearVertical.m +// QMUIKit +// +// Created by QMUI Team on 2024/1/2. +// + +#import "QMUILayouterLinearVertical.h" +#import "QMUICore.h" +#import "NSString+QMUI.h" + +@implementation QMUILayouterLinearVertical + ++ (instancetype)itemWithChildItems:(NSArray *)childItems spacingBetweenItems:(CGFloat)spacingBetweenItems { + return [self itemWithChildItems:childItems spacingBetweenItems:spacingBetweenItems horizontal:QMUILayouterAlignmentLeading vertical:QMUILayouterAlignmentLeading]; +} + ++ (instancetype)itemWithChildItems:(NSArray *)childItems spacingBetweenItems:(CGFloat)spacingBetweenItems horizontal:(QMUILayouterAlignment)horizontal vertical:(QMUILayouterAlignment)vertical { + QMUILayouterLinearVertical *item = [[self alloc] init]; + item.childItems = childItems; + item.spacingBetweenItems = spacingBetweenItems; + item.childHorizontalAlignment = horizontal; + item.childVerticalAlignment = vertical; + return item; +} + +- (NSString *)description { + NSString * (^alignmentName)(QMUILayouterAlignment alignment) = ^NSString *(QMUILayouterAlignment alignment) { + return @[@"Leading", @"Trailing", @"Center", @"Fill"][alignment]; + }; + return [NSString qmui_stringByConcat:[super description], @", horizontal = ", alignmentName(self.childHorizontalAlignment), @", vertical = ", alignmentName(self.childVerticalAlignment), nil]; +} + +// 容器性质的 layouter,不存在关联的实体 view,则始终认为是可视的,如果是 parentItem 的 parentItem 不可见,则由 parentItem 自己去管 +- (BOOL)visible { + if (self.visibleBlock) return self.visibleBlock(self); + return self.visibleChildItems.count; +} + +- (CGSize)sizeThatFits:(CGSize)size shouldConsiderBlock:(BOOL)shouldConsiderBlock { + NSArray *childItems = self.visibleChildItems; + if (!childItems.count) return self.minimumSize; + if (CGSizeEqualToSize(self.frame.size, size) || CGSizeIsEmpty(size)) { + size = CGSizeMax; + } + __block CGSize contentSize = CGSizeZero; + __block CGFloat totalShrink = QMUILayouterShrinkNever; + __block NSMutableDictionary *cachedSize = NSMutableDictionary.new; + [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + CGSize s = [obj sizeThatFits:CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(obj.margin), CGFLOAT_MAX)]; + cachedSize[[NSString stringWithFormat:@"%p", obj]] = [NSValue valueWithCGSize:s]; + contentSize.width = MAX(contentSize.width, s.width + UIEdgeInsetsGetHorizontalValue(obj.margin)); + contentSize.height += s.height + UIEdgeInsetsGetVerticalValue(obj.margin) + self.spacingBetweenItems; + + if (obj.shrink > QMUILayouterShrinkNever) { + totalShrink += s.height * obj.shrink; + } + }]; + contentSize.height -= self.spacingBetweenItems; + if (contentSize.height <= size.height || totalShrink == QMUILayouterShrinkNever) { + if (shouldConsiderBlock && self.sizeThatFitsBlock) { + contentSize = self.sizeThatFitsBlock(self, size, contentSize); + } + contentSize.width = MIN(self.maximumSize.width, MAX(self.minimumSize.width, contentSize.width)); + contentSize.height = MIN(self.maximumSize.height, MAX(self.minimumSize.height, contentSize.height)); + return contentSize; + } + + __block CGSize resultSize = CGSizeZero; + [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + CGSize s = cachedSize[[NSString stringWithFormat:@"%p", obj]].CGSizeValue; + if (obj.shrink > QMUILayouterShrinkNever) { + CGFloat spaceToShrink = contentSize.height - size.height; + CGFloat h = s.height - spaceToShrink * s.height * obj.shrink / totalShrink; + s.height = h; + } + resultSize.width = MAX(resultSize.width, s.width + UIEdgeInsetsGetHorizontalValue(obj.margin)); + resultSize.height += s.height + UIEdgeInsetsGetVerticalValue(obj.margin) + self.spacingBetweenItems; + }]; + resultSize.height -= self.spacingBetweenItems; + if (shouldConsiderBlock && self.sizeThatFitsBlock) { + resultSize = self.sizeThatFitsBlock(self, size, resultSize); + } + resultSize.width = MIN(self.maximumSize.width, MAX(self.minimumSize.width, resultSize.width)); + resultSize.height = MIN(self.maximumSize.height, MAX(self.minimumSize.height, resultSize.height)); + return resultSize; +} + +- (void)layout { + NSArray *childItems = self.visibleChildItems; + CGSize contentSize = [self sizeThatFits:CGSizeMake(CGRectGetWidth(self.frame), CGFLOAT_MAX) shouldConsiderBlock:NO]; + __block CGFloat totalGrow = QMUILayouterGrowNever; + __block CGFloat spaceToGrow = CGRectGetHeight(self.frame);// 父容器里待填充的多余空间(容器总大小减去所有固定的值,包括 spacingBetweenItems、所有 item 的 margin 区域、grow = Never 的 item 的 width之后,剩下的空间) + __block CGFloat totalShrink = QMUILayouterShrinkNever; + [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + CGFloat itemMaxWidth = CGRectGetWidth(self.frame) - UIEdgeInsetsGetHorizontalValue(obj.margin); + CGSize itemSize = [obj sizeThatFits:CGSizeMake(itemMaxWidth, CGFLOAT_MAX)]; + itemSize.width = MIN(itemMaxWidth, itemSize.width); + obj.frame = CGRectSetSize(obj.frame, itemSize); + + spaceToGrow -= CGRectGetHeight(obj.frame) + UIEdgeInsetsGetVerticalValue(obj.margin) + self.spacingBetweenItems; + if (obj.grow > QMUILayouterGrowNever) { + totalGrow += obj.grow; + } + + if (obj.shrink > QMUILayouterShrinkNever) { + totalShrink += CGRectGetHeight(obj.frame) * obj.shrink; + } + }]; + spaceToGrow += self.spacingBetweenItems; + BOOL shouldCalcGrow = totalGrow > QMUILayouterGrowNever && contentSize.height < CGRectGetHeight(self.frame); + BOOL shouldCalcShrink = totalShrink > QMUILayouterShrinkNever && contentSize.height > CGRectGetHeight(self.frame); + + __block CGFloat minX = CGRectGetMinX(self.frame); + __block CGFloat minY = CGRectGetMinY(self.frame); + __block CGFloat maxX = CGRectGetMaxX(self.frame); + __block CGFloat maxY = CGRectGetMaxY(self.frame); + QMUILayouterAlignment childVerticalAlignment = self.childVerticalAlignment; + QMUILayouterAlignment childHorizontalAlignment = self.childHorizontalAlignment; + + // 不需要考虑 grow/shrink 的情况,先把 minX 算出来 + if (!shouldCalcGrow && !shouldCalcShrink && childVerticalAlignment != QMUILayouterAlignmentLeading) { + if (contentSize.height >= CGRectGetHeight(self.frame)) { + // 不管哪种布局方式,只要内容超过容器,统一按 Leading 处理 + childVerticalAlignment = QMUILayouterAlignmentLeading; + } else if (childVerticalAlignment == QMUILayouterAlignmentTrailing) { + minY = MAX(minY, CGRectGetMaxY(self.frame) - contentSize.height); + childVerticalAlignment = QMUILayouterAlignmentLeading; + } else if (childVerticalAlignment == QMUILayouterAlignmentCenter) { + minY = MAX(minY, minY + CGFloatGetCenter(CGRectGetHeight(self.frame), contentSize.height)); + childVerticalAlignment = QMUILayouterAlignmentLeading; + } else if (childVerticalAlignment == QMUILayouterAlignmentFill) { + if (childItems.count > 1) { + // 与容器相同方向的 Fill 仅在只有一个子元素时有效,超过一个子元素则视为 Leading + // 如果你希望多个 childItem 可拉伸铺满,应该用 childItem.grow 来控制,而不是用 Fill + childVerticalAlignment = QMUILayouterAlignmentLeading; + } else { + // 一个子元素的情况,直接布局掉算了 + QMUILayouterItem *obj = self.visibleChildItem0; + obj.frame = CGRectSetY(obj.frame, minY + obj.margin.top); + obj.frame = CGRectSetHeight(obj.frame, maxY - obj.margin.bottom - CGRectGetMinY(obj.frame)); + } + } + } + + [childItems enumerateObjectsUsingBlock:^(QMUILayouterItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + obj.frame = CGRectSetY(obj.frame, minY + obj.margin.top); + if (shouldCalcGrow && obj.grow > QMUILayouterGrowNever) { + CGFloat h = CGRectGetHeight(obj.frame) + spaceToGrow * obj.grow / totalGrow; + obj.frame = CGRectSetHeight(obj.frame, h); + } + if (shouldCalcShrink && obj.shrink > QMUILayouterShrinkNever) { + CGFloat spaceToShrink = contentSize.height - CGRectGetHeight(self.frame); + CGFloat h = CGRectGetHeight(obj.frame) - spaceToShrink * CGRectGetHeight(obj.frame) * obj.shrink / totalShrink; + h = MAX(0, h); + obj.frame = CGRectSetHeight(obj.frame, h); + } + if (CGRectGetMaxY(obj.frame) + obj.margin.bottom > maxY) { + obj.frame = CGRectSetHeight(obj.frame, maxY - obj.margin.bottom - CGRectGetMinY(obj.frame)); + } + + minY = CGRectGetMaxY(obj.frame) + obj.margin.bottom + self.spacingBetweenItems; + + if (childHorizontalAlignment == QMUILayouterAlignmentTrailing) { + obj.frame = CGRectSetX(obj.frame, maxX - obj.margin.right - CGRectGetWidth(obj.frame)); + } else if (childHorizontalAlignment == QMUILayouterAlignmentCenter) { + obj.frame = CGRectSetX(obj.frame, minX + obj.margin.left + CGFloatGetCenter(maxX - minX - UIEdgeInsetsGetHorizontalValue(obj.margin), CGRectGetWidth(obj.frame))); + } else if (childHorizontalAlignment == QMUILayouterAlignmentFill) { + obj.frame = CGRectSetX(obj.frame, minX + obj.margin.left); + obj.frame = CGRectSetWidth(obj.frame, maxX - obj.margin.right - CGRectGetMinX(obj.frame)); + } else { + obj.frame = CGRectSetX(obj.frame, minX + obj.margin.left); + } + + [obj layoutIfNeeded]; + }]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILog.h b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILog.h index c2a83450..ea46e130 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILog.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILog.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILog.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.h b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.h index 086d051f..3455dbf5 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILogItem.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.m b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.m index a0135423..82fa943d 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILogItem.m @@ -20,7 +20,7 @@ @implementation QMUILogItem + (instancetype)logItemWithLevel:(QMUILogLevel)level name:(NSString *)name logString:(NSString *)logString, ... { - QMUILogItem *logItem = [[QMUILogItem alloc] init]; + QMUILogItem *logItem = [[self alloc] init]; logItem.level = level; logItem.name = name; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.h b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.h index 6808e1c6..918ee55e 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILogNameManager.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.m b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.m index 614ebcd0..972c14bd 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILogNameManager.m diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.h b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.h index d47c86d0..1959beb7 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILogger.h // QMUIKit diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.m b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.m index b6eaf534..71e52051 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILogger.m diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.h b/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.h index be6b35be..bf04276d 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILogManagerViewController.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.m index 0d09fbbe..dead13a4 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILogManagerViewController.m @@ -25,13 +25,13 @@ #import "QMUITableViewCell.h" #import "QMUISearchController.h" #import "UIBarItem+QMUI.h" +#import "UIViewController+QMUI.h" @interface QMUILogManagerViewController () @property(nonatomic, copy) NSDictionary *allNames; @property(nonatomic, copy) NSArray *sortedLogNames; @property(nonatomic, copy) NSArray *sectionIndexTitles; -@property(nonatomic, assign) UIStatusBarStyle statusBarStyle; @end @implementation QMUILogManagerViewController @@ -39,7 +39,6 @@ @implementation QMUILogManagerViewController - (void)didInitializeWithStyle:(UITableViewStyle)style { [super didInitializeWithStyle:style]; self.rowCountWhenShowSearchBar = 10; - self.statusBarStyle = QMUICMIActivated ? (StatusbarStyleLightInitially ? UIStatusBarStyleLightContent : UIStatusBarStyleDefault) : UIStatusBarStyleDefault; } - (void)initTableView { @@ -47,6 +46,13 @@ - (void)initTableView { [self setupDataSource]; } +- (void)initSearchController { + [super initSearchController]; + self.searchController.qmui_preferredStatusBarStyleBlock = ^UIStatusBarStyle{ + return UIStatusBarStyleDarkContent; + }; +} + - (void)viewDidLoad { [super viewDidLoad]; [self checkEmptyView]; @@ -61,10 +67,6 @@ - (void)setupNavigationItems { } } -- (UIStatusBarStyle)preferredStatusBarStyle { - return self.statusBarStyle; -} - - (void)setupDataSource { self.allNames = [QMUILogger sharedInstance].logNameManager.allNames; @@ -166,25 +168,25 @@ - (void)handleMenuItemEvent { menuView.maximumWidth = 124; menuView.safetyMarginsOfSuperview = UIEdgeInsetsSetRight(menuView.safetyMarginsOfSuperview, 6); menuView.items = @[ - [QMUIPopupMenuButtonItem itemWithImage:nil title:@"开启全部" handler:^(QMUIPopupMenuButtonItem *aItem) { - for (NSString *logName in self.allNames) { - [[QMUILogger sharedInstance].logNameManager setEnabled:YES forLogName:logName]; - } - [self reloadData]; - [aItem.menuView hideWithAnimated:YES]; - }], - [QMUIPopupMenuButtonItem itemWithImage:nil title:@"禁用全部" handler:^(QMUIPopupMenuButtonItem *aItem) { - for (NSString *logName in self.allNames) { - [[QMUILogger sharedInstance].logNameManager setEnabled:NO forLogName:logName]; - } - [self reloadData]; - [aItem.menuView hideWithAnimated:YES]; - }], - [QMUIPopupMenuButtonItem itemWithImage:nil title:@"清空全部" handler:^(QMUIPopupMenuButtonItem *aItem) { - [[QMUILogger sharedInstance].logNameManager removeAllNames]; - [self reloadData]; - [aItem.menuView hideWithAnimated:YES]; - }]]; + [QMUIPopupMenuItem itemWithTitle:@"开启全部" handler:^(__kindof QMUIPopupMenuItem * _Nonnull aItem, __kindof UIControl * _Nonnull aItemView, NSInteger section, NSInteger index) { + for (NSString *logName in self.allNames) { + [[QMUILogger sharedInstance].logNameManager setEnabled:YES forLogName:logName]; + } + [self reloadData]; + [aItem.menuView hideWithAnimated:YES]; + }], + [QMUIPopupMenuItem itemWithTitle:@"禁用全部" handler:^(__kindof QMUIPopupMenuItem * _Nonnull aItem, __kindof UIControl * _Nonnull aItemView, NSInteger section, NSInteger index) { + for (NSString *logName in self.allNames) { + [[QMUILogger sharedInstance].logNameManager setEnabled:NO forLogName:logName]; + } + [self reloadData]; + [aItem.menuView hideWithAnimated:YES]; + }], + [QMUIPopupMenuItem itemWithTitle:@"清空全部" handler:^(__kindof QMUIPopupMenuItem * _Nonnull aItem, __kindof UIControl * _Nonnull aItemView, NSInteger section, NSInteger index) { + [[QMUILogger sharedInstance].logNameManager removeAllNames]; + [self reloadData]; + [aItem.menuView hideWithAnimated:YES]; + }]]; menuView.sourceBarItem = self.navigationItem.rightBarButtonItem; [menuView showWithAnimated:YES]; } @@ -247,17 +249,9 @@ - (void)searchController:(QMUISearchController *)searchController updateResultsF } } -- (void)willPresentSearchController:(QMUISearchController *)searchController { - self.statusBarStyle = UIStatusBarStyleDefault; - [self setNeedsStatusBarAppearanceUpdate]; -} - - (void)willDismissSearchController:(QMUISearchController *)searchController { - // 在搜索状态里可能修改了 switch 的值,则退出时强制刷新一下默认状态的列表 [self reloadData]; - self.statusBarStyle = QMUICMIActivated ? (StatusbarStyleLightInitially ? UIStatusBarStyleLightContent : UIStatusBarStyleDefault) : UIStatusBarStyleDefault; - [self setNeedsStatusBarAppearanceUpdate]; } @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.h b/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.h index e448eefc..8fc9b904 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILogger+QMUIConfigurationTemplate.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.m b/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.m index ba2d22ed..04a54869 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILogger+QMUIConfigurationTemplate.m @@ -23,13 +23,10 @@ + (void)load { dispatch_once(&onceToken, ^{ OverrideImplementation([QMUILogger class], @selector(printLogWithFile:line:func:logItem:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(QMUILogger *selfObject, const char *file, int line, const char *func, QMUILogItem *logItem) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - // 不同级别的 log 可通过配置表的开关来控制是否要输出 - if (logItem.level == QMUILogLevelDefault && !ShouldPrintDefaultLog) return; - if (logItem.level == QMUILogLevelInfo && !ShouldPrintInfoLog) return; - if (logItem.level == QMUILogLevelWarn && !ShouldPrintWarnLog) return; - } + // 不同级别的 log 可通过配置表的开关来控制是否要输出 + if (logItem.level == QMUILogLevelDefault && !ShouldPrintDefaultLog) return; + if (logItem.level == QMUILogLevelInfo && !ShouldPrintInfoLog) return; + if (logItem.level == QMUILogLevelWarn && !ShouldPrintWarnLog) return; // call super void (*originSelectorIMP)(id, SEL, const char *, int, const char *, QMUILogItem *); diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.h b/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.h index 6ed6b2fd..95faf9a8 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIMarqueeLabel.h @@ -65,3 +65,29 @@ */ - (BOOL)requestToStopAnimation; @end + + +@interface UILabel (QMUI_Marquee) + +/** + 是否开启系统自带的跑马灯效果(系统的只能控制开启/关闭,无法控制速度、停顿等,更多功能可以使用 @c QMUIMarqueeLabel ,但论性能还是系统的更优。 + + 用法: + [label qmui_startNativeMarquee]; + [label qmui_stopNativeMarquee]; // 当你需要停止动画时,调用这个方法(如果业务只关心什么时候开启,不关心什么时候结束,则从头到尾都可以不用调用这个方法) + + @note 当开启该属性时,会强制把 numberOfLines 设置为1,clipsToBounds 设置为 YES。如果你是在 reuse view 内使用(例如 UITableViewCell/UICollectionViewCell),需要手动在 will display 时 start,did end display 时 stop。 + */ +- (void)qmui_startNativeMarquee; + +/** + 停止跑马灯效果,与 @c qmui_startNativeMarquee 不需要成对出现,也即如果业务不关心什么时候停止动画,可以从头到尾都不调用这个方法。 + */ +- (void)qmui_stopNativeMarquee; + +/** + 系统的跑马灯效果是否正在运行,默认为 NO。 + */ +@property(nonatomic, assign, readonly) BOOL qmui_nativeMarqueeRunning; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.m b/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.m index c66b09da..c377270e 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIMarqueeLabel.m @@ -232,6 +232,7 @@ - (void)checkIfShouldShowGradientLayer { self.fadeLayer.endPoint = CGPointMake(1, .5); self.fadeLayer.colors = @[(id)UIColorMakeWithRGBA(255, 255, 255, 0).CGColor, (id)UIColorMakeWithRGBA(255, 255, 255, 1).CGColor, (id)UIColorMakeWithRGBA(255, 255, 255, 1).CGColor, (id)UIColorMakeWithRGBA(255, 255, 255, 0).CGColor]; self.layer.mask = self.fadeLayer; + [self setNeedsLayout];// fadeLayer 作为 layer.mask,它依赖于在 layoutSubviews 里正确布局,否则会因为错误的 size 而导致 label 看不见 } else { if (self.layer.mask == self.fadeLayer) { self.layer.mask = nil; @@ -265,3 +266,49 @@ - (BOOL)requestToStopAnimation { } @end + +@implementation UILabel (QMUI_Marquee) + +- (void)qmui_startNativeMarquee { + // 系统有 _startMarqueeIfNecessary、_startMarquee,但直接开启的方法其实是 marqueeRunning + BOOL running = YES; + self.numberOfLines = 1; + self.clipsToBounds = YES; + [self qmui_performSelector:NSSelectorFromString(@"setMarqueeEnabled:") withArguments:&running, nil]; + [self qmui_performSelector:NSSelectorFromString(@"setMarqueeRunning:") withArguments:&running, nil]; + [self qmuimq_removeObserver]; + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(qmuimq_handleApplicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(qmuimq_handleApplicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; +} + +- (void)qmui_stopNativeMarquee { + // 系统有 _stopMarqueeWithRedisplay:,但直接关闭的方法其实是 marqueeRunning + BOOL running = NO; + [self qmui_performSelector:NSSelectorFromString(@"setMarqueeRunning:") withArguments:&running, nil]; + [self qmui_performSelector:NSSelectorFromString(@"setMarqueeEnabled:") withArguments:&running, nil]; + [self qmuimq_removeObserver]; +} + +- (BOOL)qmui_nativeMarqueeRunning { + BOOL running = NO; + [self qmui_performSelector:NSSelectorFromString(@"marqueeRunning") withPrimitiveReturnValue:&running]; + return running; +} + +- (void)qmuimq_handleApplicationDidEnterBackground:(NSNotification *)notification { + [self qmui_bindBOOL:self.qmui_nativeMarqueeRunning forKey:@"QMUI_Marquee_Running"]; +} + +- (void)qmuimq_handleApplicationDidBecomeActive:(NSNotification *)notification { + if ([self qmui_getBoundBOOLForKey:@"QMUI_Marquee_Running"]) { + [self qmui_stopNativeMarquee];// 要手动停止一次才能重新 start + [self qmui_startNativeMarquee]; + } +} + +- (void)qmuimq_removeObserver { + [NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; + [NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h b/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h index e8c5f8f7..b44011fa 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIModalPresentationViewController.h @@ -46,9 +46,9 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) { @optional /** - * 是否应该隐藏浮层,会在调用`hideWithAnimated:completion:`时,以及点击背景遮罩时被调用。默认为YES。 - * @param controller 当前的modalController - * @return 是否允许隐藏,YES表示允许隐藏,NO表示不允许隐藏 + * 是否应该隐藏浮层,默认为YES,会在代码主动调用隐藏,或点击背景遮罩时询问。 + * @param controller 当前的 modalController + * @return 是否允许隐藏,YES 表示允许隐藏,NO 表示不允许隐藏 */ - (BOOL)shouldHideModalPresentationViewController:(QMUIModalPresentationViewController *)controller; @@ -137,6 +137,24 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) { */ @property(nonatomic, assign) CGFloat maximumContentViewWidth UI_APPEARANCE_SELECTOR; +/** + 如果 modal 是以 window 形式显示的话,通过这个属性可以获取内部实际在用的 window 对象。 + */ +@property(nullable, nonatomic, strong, readonly) UIWindow *window; + +/** + 如果 modal 是以 window 形式显示的话,通过这个属性来决定 window 是否需要以 keyWindow 形式存在(keyWindow 一般用于与键盘交互的场景,没输入框可以不用开启它) + 默认为 YES。 + */ +@property(nonatomic, assign) BOOL shouldBecomeKeyWindow; + +/** + 如果 modal 是以 window 形式显示的话,控制在 modal 显示时是否要自动把 App 主界面置灰。 + 默认为 YES。 + 该属性在非 window 形式显示的情况下无意义。 + */ +@property(nonatomic, assign) BOOL shouldDimmedAppAutomatically; + /** * 背景遮罩,默认为一个普通的`UIView`,背景色为`UIColorMask`,可设置为自己的view,注意`dimmingView`的大小将会盖满整个控件。 * @@ -145,7 +163,12 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) { @property(nullable, nonatomic, strong) IBOutlet UIView *dimmingView; /** - * 由于点击遮罩导致浮层被隐藏时的回调(区分于`hideWithAnimated:completion:`里的completion,这里是特地用于点击遮罩的情况) + * 由于点击遮罩导致浮层即将被隐藏的回调 + */ +@property(nullable, nonatomic, copy) void (^willHideByDimmingViewTappedBlock)(void); + +/** + * 由于点击遮罩导致浮层被隐藏后的回调(区分于`hideWithAnimated:completion:`里的completion,这里是特地用于点击遮罩的情况) */ @property(nullable, nonatomic, copy) void (^didHideByDimmingViewTappedBlock)(void); @@ -186,7 +209,7 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) { @property(nonatomic, assign) BOOL onlyRespondsToKeyboardEventFromDescendantViews; /** - * 管理自定义的浮层布局,将会在浮层显示前、控件的容器大小发生变化时(例如横竖屏、来电状态栏)被调用 + * 管理自定义的浮层布局,将会在浮层显示前、控件的容器大小发生变化时(例如横竖屏、来电状态栏)被调用,请在 block 内主动为 view 设置期望的 frame,设置时建议用 qmui_frameApplyTransform 取代 setFrame:,否则在有键盘的情况下,显隐动画可能有错。 * @arg containerBounds 浮层所在的父容器的大小,也即`self.view.bounds` * @arg keyboardHeight 键盘在当前界面里的高度,若无键盘,则为0 * @arg contentViewDefaultFrame 不使用自定义布局的情况下的默认布局,会受`contentViewMargins`、`maximumContentViewWidth`、`contentView sizeThatFits:`的影响 @@ -291,7 +314,7 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) { @end -/// 专用于QMUIModalPresentationViewController的UIWindow,这样才能在`[[UIApplication sharedApplication] windows]`里方便地区分出来 +/// 专用于QMUIModalPresentationViewController的UIWindow,这样才能在`UIApplication.sharedApplication.windows`里方便地区分出来 @interface QMUIModalPresentationWindow : UIWindow @end @@ -300,7 +323,7 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) { @interface UIViewController (QMUIModalPresentationViewController) /** - * 获取弹出当前 vieController 的 QMUIModalPresentationViewController,注意需要 modalPresentationViewController show 之后这个属性才会被赋值。 + * 获取弹出当前 vieController 的 QMUIModalPresentationViewController */ @property(nullable, nonatomic, weak, readonly) QMUIModalPresentationViewController *qmui_modalPresentationViewController; @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m index f252bca3..5355412f 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIModalPresentationViewController.m @@ -19,6 +19,7 @@ #import "UIView+QMUI.h" #import "QMUIKeyboardManager.h" #import "UIWindow+QMUI.h" +#import "QMUIAppearance.h" @interface UIViewController () @@ -27,36 +28,29 @@ @interface UIViewController () @implementation QMUIModalPresentationViewController (UIAppearance) -static QMUIModalPresentationViewController *appearance; -+ (nonnull instancetype)appearance { ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; +} + ++ (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - [self initDefaultAppearance]; + [self initAppearance]; }); - return appearance; } -+ (void)initDefaultAppearance { - if (!appearance) { - appearance = [[self alloc] init]; - } ++ (void)initAppearance { + QMUIModalPresentationViewController *appearance = QMUIModalPresentationViewController.appearance; appearance.animationStyle = QMUIModalPresentationAnimationStyleFade; appearance.contentViewMargins = UIEdgeInsetsMake(20, 20, 20, 20); appearance.maximumContentViewWidth = CGFLOAT_MAX; } -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self appearance]; - }); -} - @end @interface QMUIModalPresentationViewController () -@property(nonatomic, strong) QMUIModalPresentationWindow *containerWindow; +@property(nonatomic, strong, readwrite) QMUIModalPresentationWindow *window; @property(nonatomic, weak) UIWindow *previousKeyWindow; @property(nonatomic, assign, readwrite, getter=isVisible) BOOL visible; @@ -70,12 +64,17 @@ @interface QMUIModalPresentationViewController () /// 标志 modal 本身以 present 的形式显示之后,又再继续 present 了一个子界面后从子界面回来时触发的 viewWillAppear: @property(nonatomic, assign) BOOL viewWillAppearByPresentedViewController; -/// 标志是否已经走过一次viewWillAppear了,用于hideInView的情况 +/// 标志是否已经走过一次viewWillDisappear了,用于hideInView的情况 @property(nonatomic, assign) BOOL hasAlreadyViewWillDisappear; +/// 如果用 showInView 的方式显示浮层,则在浮层所在的父界面被 pop(或 push 到下一个界面)时,会自动触发 viewWillDisappear:,导致浮层被隐藏,为了保证走到 viewWillDisappear: 一定是手动调用 hide 的,就加了这个标志位 +/// https://github.com/Tencent/QMUI_iOS/issues/639 +@property(nonatomic, assign) BOOL willHideInView; + @property(nonatomic, strong) UITapGestureRecognizer *dimmingViewTapGestureRecognizer; @property(nonatomic, strong) QMUIKeyboardManager *keyboardManager; @property(nonatomic, assign) CGFloat keyboardHeight; +@property(nonatomic, assign) BOOL avoidKeyboardLayout; @end @implementation QMUIModalPresentationViewController @@ -95,27 +94,24 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { } - (void)didInitialize { - if (appearance) { - self.animationStyle = appearance.animationStyle; - self.contentViewMargins = appearance.contentViewMargins; - self.maximumContentViewWidth = appearance.maximumContentViewWidth; - self.onlyRespondsToKeyboardEventFromDescendantViews = YES; - self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; - self.modalPresentationStyle = UIModalPresentationCustom; - - // 这一段是给以 present 方式显示的浮层用的,其他方式显示的浮层,会在 supportedInterfaceOrientations 里实时获取支持的设备方向 - UIViewController *visibleViewController = [QMUIHelper visibleViewController]; - if (visibleViewController) { - self.supportedOrientationMask = visibleViewController.supportedInterfaceOrientations; - } else { - self.supportedOrientationMask = SupportedOrientationMask; - } - - if (self != appearance) { - self.keyboardManager = [[QMUIKeyboardManager alloc] initWithDelegate:self]; - [self initDefaultDimmingViewWithoutAddToView]; - } + [self qmui_applyAppearance]; + + self.shouldDimmedAppAutomatically = YES; + self.onlyRespondsToKeyboardEventFromDescendantViews = YES; + self.shouldBecomeKeyWindow = YES; + self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; + self.modalPresentationStyle = UIModalPresentationCustom; + + // 这一段是给以 present 方式显示的浮层用的,其他方式显示的浮层,会在 supportedInterfaceOrientations 里实时获取支持的设备方向 + UIViewController *visibleViewController = [QMUIHelper visibleViewController]; + if (visibleViewController) { + self.supportedOrientationMask = visibleViewController.supportedInterfaceOrientations; + } else { + self.supportedOrientationMask = SupportedOrientationMask; } + + self.keyboardManager = [[QMUIKeyboardManager alloc] initWithDelegate:self]; + [self initDefaultDimmingViewWithoutAddToView]; } - (void)awakeFromNib { @@ -127,7 +123,7 @@ - (void)awakeFromNib { } - (void)dealloc { - self.containerWindow = nil; + self.window = nil; } - (BOOL)shouldAutomaticallyForwardAppearanceMethods { @@ -158,15 +154,14 @@ - (void)viewDidLayoutSubviews { - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; + self.visible = YES;// present 模式没有入口 show 方法,只能加在这里 + if (self.shownInWindowMode) { // 只有使用showWithAnimated:completion:显示出来的浮层,才需要修改之前就记住的animated的值 animated = self.appearAnimated; } - self.keyboardManager.delegateEnabled = YES; - if (self.contentViewController) { - self.contentViewController.qmui_modalPresentationViewController = self; [self.contentViewController beginAppearanceTransition:YES animated:animated]; } @@ -176,17 +171,11 @@ - (void)viewWillAppear:(BOOL)animated { return; } - if (self.isShownInWindowMode) { - [QMUIHelper dimmedApplicationWindow]; - } - void (^didShownCompletion)(BOOL finished) = ^(BOOL finished) { if (self.contentViewController) { [self.contentViewController endAppearanceTransition]; } - self.visible = YES; - if (self.appearCompletionBlock) { self.appearCompletionBlock(finished); self.appearCompletionBlock = nil; @@ -207,6 +196,12 @@ - (void)viewWillAppear:(BOOL)animated { contentViewFrame = self.contentView.frame; } self.showingAnimation(self.dimmingView, self.view.bounds, self.keyboardHeight, contentViewFrame, didShownCompletion); + + if (self.shouldDimmedAppAutomatically) { + [UIView animateWithDuration:.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + [QMUIHelper dimmedApplicationWindow]; + } completion:nil]; + } } else { self.contentView.frame = contentViewFrame; [self.contentView setNeedsLayout]; @@ -215,10 +210,12 @@ - (void)viewWillAppear:(BOOL)animated { [self showingAnimationWithCompletion:didShownCompletion]; } } else { + if (self.shouldDimmedAppAutomatically) { + [QMUIHelper dimmedApplicationWindow]; + } CGRect contentViewFrame = [self contentViewFrameForShowing]; self.contentView.frame = contentViewFrame; [self.view addSubview:self.contentView]; - self.dimmingView.alpha = 1; didShownCompletion(YES); } } @@ -237,6 +234,12 @@ - (void)viewWillDisappear:(BOOL)animated { return; } + /// 如果用 showInView 的方式显示浮层,则在浮层所在的父界面被 pop(或 push 到下一个界面)时,会自动触发 viewWillDisappear:,导致浮层被隐藏,为了保证走到 viewWillDisappear: 一定是手动调用 hide 的,就用 willHideInView 来区分。 + /// https://github.com/Tencent/QMUI_iOS/issues/639 + if (self.shownInSubviewMode && !self.willHideInView) { + return; + } + [super viewWillDisappear:animated]; if (self.shownInWindowMode) { @@ -251,8 +254,8 @@ - (void)viewWillDisappear:(BOOL)animated { } } - // 在降下键盘前取消对键盘事件的监听,从而避免键盘影响隐藏浮层的动画 - self.keyboardManager.delegateEnabled = NO; + // 先更新标志位再 endEditing,保证键盘降下时不触发 updateLayout,从而避免影响 hidingAnimation 的动画 + self.avoidKeyboardLayout = YES; [self.view endEditing:YES]; if (self.contentViewController) { @@ -264,32 +267,34 @@ - (void)viewWillDisappear:(BOOL)animated { return; } - if (self.isShownInWindowMode) { - [QMUIHelper resetDimmedApplicationWindow]; - } - void (^didHiddenCompletion)(BOOL finished) = ^(BOOL finished) { if (self.shownInWindowMode) { // 恢复 keyWindow 之前做一下检查,避免这个问题 https://github.com/Tencent/QMUI_iOS/issues/90 - if ([[UIApplication sharedApplication] keyWindow] == self.containerWindow) { + if (UIApplication.sharedApplication.keyWindow == self.window) { if (self.previousKeyWindow.hidden) { // 保护了这个 issue 记录的情况,避免主 window 丢失 keyWindow https://github.com/Tencent/QMUI_iOS/issues/315 - [[UIApplication sharedApplication].delegate.window makeKeyWindow]; + [UIApplication.sharedApplication.delegate.window makeKeyWindow]; } else { [self.previousKeyWindow makeKeyWindow]; } } - self.containerWindow.hidden = YES; - self.containerWindow.rootViewController = nil; + self.window.hidden = YES; + self.window.rootViewController = nil; self.previousKeyWindow = nil; [self endAppearanceTransition]; } if (self.shownInSubviewMode) { - // 这句是给addSubview的形式显示的情况下使用,但会触发第二次viewWillDisappear:,所以要搭配self.hasAlreadyViewWillDisappear使用 + self.willHideInView = NO; + [self.view removeFromSuperview]; - self.hasAlreadyViewWillDisappear = NO; + + // removeFromSuperview 在 animated:YES 时会触发第二次viewWillDisappear:,所以要搭配self.hasAlreadyViewWillDisappear使用 + // animated:NO 不会触发 + if (animated) { + self.hasAlreadyViewWillDisappear = NO; + } } [self.contentView removeFromSuperview]; @@ -298,6 +303,7 @@ - (void)viewWillDisappear:(BOOL)animated { } self.visible = NO; + self.avoidKeyboardLayout = NO; if ([self.delegate respondsToSelector:@selector(didHideModalPresentationViewController:)]) { [self.delegate didHideModalPresentationViewController:self]; @@ -308,21 +314,24 @@ - (void)viewWillDisappear:(BOOL)animated { self.disappearCompletionBlock = nil; } - if (self.contentViewController) { - self.contentViewController.qmui_modalPresentationViewController = nil; - self.contentViewController = nil; - } - self.disappearAnimated = NO; }; if (animated) { if (self.hidingAnimation) { self.hidingAnimation(self.dimmingView, self.view.bounds, self.keyboardHeight, didHiddenCompletion); + if (self.shouldDimmedAppAutomatically) { + [UIView animateWithDuration:.25 delay:0 options:QMUIViewAnimationOptionsCurveIn animations:^{ + [QMUIHelper resetDimmedApplicationWindow]; + } completion:nil]; + } } else { [self hidingAnimationWithCompletion:didHiddenCompletion]; } } else { + if (self.shouldDimmedAppAutomatically) { + [QMUIHelper resetDimmedApplicationWindow]; + } didHiddenCompletion(YES); } } @@ -344,6 +353,10 @@ - (void)updateLayout { } } +- (BOOL)shouldDimmedAppAutomatically { + return _shouldDimmedAppAutomatically && self.isShownInWindowMode; +} + #pragma mark - Dimming View - (void)setDimmingView:(UIView *)dimmingView { @@ -397,8 +410,17 @@ - (void)handleDimmingViewTapGestureRecognizer:(UITapGestureRecognizer *)tapGestu if (weakSelf.didHideByDimmingViewTappedBlock) { weakSelf.didHideByDimmingViewTappedBlock(); } - }]; + } sender:tapGestureRecognizer]; } else if (self.shownInPresentedMode) { + // 这里仅屏蔽点击遮罩时的 dismiss,如果是代码手动调用 dismiss 的,在 UIViewController(QMUIModalPresentationViewController) 里会通过重写 dismiss 方法来屏蔽。 + // 为什么不能统一交给 UIViewController(QMUIModalPresentationViewController) 里屏蔽,是因为点击遮罩触发的 dismiss 要调用 willHideByDimmingViewTappedBlock,而 UIViewController 那边不知道此次 dismiss 是否由点击遮罩触发的,所以分开两边写。 + if ([self.delegate respondsToSelector:@selector(shouldHideModalPresentationViewController:)] && ![self.delegate shouldHideModalPresentationViewController:self]) { + return; + } + if (self.willHideByDimmingViewTappedBlock) { + self.willHideByDimmingViewTappedBlock(); + } + [self dismissViewControllerAnimated:YES completion:^{ if (self.didHideByDimmingViewTappedBlock) { self.didHideByDimmingViewTappedBlock(); @@ -410,13 +432,17 @@ - (void)handleDimmingViewTapGestureRecognizer:(UITapGestureRecognizer *)tapGestu if (weakSelf.didHideByDimmingViewTappedBlock) { weakSelf.didHideByDimmingViewTappedBlock(); } - }]; + } sender:tapGestureRecognizer]; } } #pragma mark - ContentView - (void)setContentViewController:(UIViewController *)contentViewController { + if (![contentViewController isEqual:_contentViewController]) { + _contentViewController.qmui_modalPresentationViewController = nil; + } + contentViewController.qmui_modalPresentationViewController = self; _contentViewController = contentViewController; self.contentView = contentViewController.view; } @@ -427,9 +453,12 @@ - (void)showingAnimationWithCompletion:(void (^)(BOOL))completion { if (self.animationStyle == QMUIModalPresentationAnimationStyleFade) { self.dimmingView.alpha = 0.0; self.contentView.alpha = 0.0; - [UIView animateWithDuration:.2 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ self.dimmingView.alpha = 1.0; self.contentView.alpha = 1.0; + if (self.shouldDimmedAppAutomatically) { + [QMUIHelper dimmedApplicationWindow]; + } } completion:^(BOOL finished) { if (completion) { completion(finished); @@ -439,9 +468,12 @@ - (void)showingAnimationWithCompletion:(void (^)(BOOL))completion { } else if (self.animationStyle == QMUIModalPresentationAnimationStylePopup) { self.dimmingView.alpha = 0.0; self.contentView.transform = CGAffineTransformMakeScale(0, 0); - [UIView animateWithDuration:.3 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ self.dimmingView.alpha = 1.0; - self.contentView.transform = CGAffineTransformMakeScale(1, 1); + self.contentView.transform = CGAffineTransformIdentity; + if (self.shouldDimmedAppAutomatically) { + [QMUIHelper dimmedApplicationWindow]; + } } completion:^(BOOL finished) { self.contentView.transform = CGAffineTransformIdentity; if (completion) { @@ -452,9 +484,12 @@ - (void)showingAnimationWithCompletion:(void (^)(BOOL))completion { } else if (self.animationStyle == QMUIModalPresentationAnimationStyleSlide) { self.dimmingView.alpha = 0.0; self.contentView.transform = CGAffineTransformMakeTranslation(0, CGRectGetHeight(self.view.bounds) - CGRectGetMinY(self.contentView.frame)); - [UIView animateWithDuration:.3 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ self.dimmingView.alpha = 1.0; self.contentView.transform = CGAffineTransformIdentity; + if (self.shouldDimmedAppAutomatically) { + [QMUIHelper dimmedApplicationWindow]; + } } completion:^(BOOL finished) { if (completion) { completion(finished); @@ -464,46 +499,66 @@ - (void)showingAnimationWithCompletion:(void (^)(BOOL))completion { } - (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { + if (self.visible) return; + self.visible = YES; + // makeKeyAndVisible 导致的 viewWillAppear: 必定 animated 是 NO 的,所以这里用额外的变量保存这个 animated 的值 self.appearAnimated = animated; self.appearCompletionBlock = completion; - self.previousKeyWindow = [UIApplication sharedApplication].keyWindow; - if (!self.containerWindow) { - self.containerWindow = [[QMUIModalPresentationWindow alloc] init]; - self.containerWindow.qmui_capturesStatusBarAppearance = NO;// modalPrensetationViewController.contentViewController 默认无权管理状态栏的样式,如需修改状态栏,请业务自己将这个属性改为 YES - self.containerWindow.windowLevel = UIWindowLevelQMUIAlertView; - self.containerWindow.backgroundColor = UIColorClear;// 避免横竖屏旋转时出现黑色 + self.previousKeyWindow = UIApplication.sharedApplication.keyWindow; + if (!self.window) { + self.window = [[QMUIModalPresentationWindow alloc] init]; + self.window.windowLevel = UIWindowLevelQMUIAlertView; + self.window.backgroundColor = UIColorClear;// 避免横竖屏旋转时出现黑色 + [self updateWindowStatusBarCapture]; + } + self.window.rootViewController = self; + if (self.shouldBecomeKeyWindow) { + [self.window makeKeyAndVisible]; + } else { + self.window.hidden = NO; } - self.containerWindow.rootViewController = self; - [self.containerWindow makeKeyAndVisible]; } - (void)hidingAnimationWithCompletion:(void (^)(BOOL))completion { if (self.animationStyle == QMUIModalPresentationAnimationStyleFade) { - [UIView animateWithDuration:.2 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ self.dimmingView.alpha = 0.0; self.contentView.alpha = 0.0; + if (self.shouldDimmedAppAutomatically) { + [QMUIHelper resetDimmedApplicationWindow]; + } } completion:^(BOOL finished) { if (completion) { + self.dimmingView.alpha = 1.0; + self.contentView.alpha = 1.0; completion(finished); } }]; } else if (self.animationStyle == QMUIModalPresentationAnimationStylePopup) { - [UIView animateWithDuration:.3 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ self.dimmingView.alpha = 0.0; - self.contentView.transform = CGAffineTransformMakeScale(0.0, 0.0); + self.contentView.transform = CGAffineTransformMakeScale(0.01, 0.01); + if (self.shouldDimmedAppAutomatically) { + [QMUIHelper resetDimmedApplicationWindow]; + } } completion:^(BOOL finished) { if (completion) { + self.dimmingView.alpha = 1.0; self.contentView.transform = CGAffineTransformIdentity; completion(finished); } }]; } else if (self.animationStyle == QMUIModalPresentationAnimationStyleSlide) { - [UIView animateWithDuration:.3 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ self.dimmingView.alpha = 0.0; self.contentView.transform = CGAffineTransformMakeTranslation(0, CGRectGetHeight(self.view.bounds) - CGRectGetMinY(self.contentView.frame)); + if (self.shouldDimmedAppAutomatically) { + [QMUIHelper resetDimmedApplicationWindow]; + } } completion:^(BOOL finished) { if (completion) { + self.dimmingView.alpha = 1.0; self.contentView.transform = CGAffineTransformIdentity; completion(finished); } @@ -512,6 +567,12 @@ - (void)hidingAnimationWithCompletion:(void (^)(BOOL))completion { } - (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { + [self hideWithAnimated:animated completion:completion sender:nil]; +} + +- (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion sender:(id)sender { + if (!self.visible) return; + self.disappearAnimated = animated; self.disappearCompletionBlock = completion; @@ -523,6 +584,12 @@ - (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { return; } + if (sender == self.dimmingViewTapGestureRecognizer) { + if (self.willHideByDimmingViewTappedBlock) { + self.willHideByDimmingViewTappedBlock(); + } + } + // window模式下,通过手动触发viewWillDisappear:来做界面消失的逻辑 if (self.shownInWindowMode) { [self beginAppearanceTransition:NO animated:animated]; @@ -530,6 +597,9 @@ - (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { } - (void)showInView:(UIView *)view animated:(BOOL)animated completion:(void (^)(BOOL))completion { + if (self.visible) return; + self.visible = YES; + self.appearCompletionBlock = completion; [self loadViewIfNeeded]; [self beginAppearanceTransition:YES animated:animated]; @@ -538,9 +608,33 @@ - (void)showInView:(UIView *)view animated:(BOOL)animated completion:(void (^)(B } - (void)hideInView:(UIView *)view animated:(BOOL)animated completion:(void (^)(BOOL))completion { + [self hideInView:view animated:animated completion:completion sender:nil]; +} + +- (void)hideInView:(UIView *)view animated:(BOOL)animated completion:(void (^)(BOOL))completion sender:(id)sender { + if (!self.visible) return; + + BOOL shouldHide = YES; + if ([self.delegate respondsToSelector:@selector(shouldHideModalPresentationViewController:)]) { + shouldHide = [self.delegate shouldHideModalPresentationViewController:self]; + } + if (!shouldHide) { + return; + } + + self.willHideInView = YES; + + if (sender == self.dimmingViewTapGestureRecognizer) { + if (self.willHideByDimmingViewTappedBlock) { + self.willHideByDimmingViewTappedBlock(); + } + } + self.disappearCompletionBlock = completion; [self beginAppearanceTransition:NO animated:animated]; - self.hasAlreadyViewWillDisappear = YES; + if (animated) { + self.hasAlreadyViewWillDisappear = YES; + } [self endAppearanceTransition]; } @@ -560,7 +654,7 @@ - (CGRect)contentViewFrameForShowing { } - (BOOL)isShownInWindowMode { - return !!self.containerWindow; + return !!self.window; } - (BOOL)isShownInPresentedMode { @@ -568,7 +662,7 @@ - (BOOL)isShownInPresentedMode { } - (BOOL)isShownInSubviewMode { - return !self.shownInPresentedMode && self.view.superview; + return !self.shownInWindowMode && !self.shownInPresentedMode && self.view.superview; } - (BOOL)isShowingPresentedViewController { @@ -584,8 +678,13 @@ - (void)keyboardWillChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUser return; } } - self.keyboardHeight = [keyboardUserInfo heightInView:self.view]; - [self updateLayout]; + CGFloat keyboardHeight = [keyboardUserInfo heightInView:self.view]; + if (self.keyboardHeight != keyboardHeight) { + self.keyboardHeight = keyboardHeight; + if (!self.avoidKeyboardLayout) { + [self updateLayout]; + } + } } #pragma mark - 屏幕旋转 @@ -606,20 +705,40 @@ - (UIInterfaceOrientationMask)supportedInterfaceOrientations { return self.supportedOrientationMask; } -- (UIViewController *)childViewControllerForStatusBarStyle { - if (self.shownInPresentedMode) { - return self.contentViewController; +- (void)setQmui_prefersStatusBarHiddenBlock:(BOOL (^)(void))qmui_prefersStatusBarHiddenBlock { + [super setQmui_prefersStatusBarHiddenBlock:qmui_prefersStatusBarHiddenBlock]; + [self updateWindowStatusBarCapture]; +} + +- (void)setQmui_preferredStatusBarStyleBlock:(UIStatusBarStyle (^)(void))qmui_preferredStatusBarStyleBlock { + [super setQmui_preferredStatusBarStyleBlock:qmui_preferredStatusBarStyleBlock]; + [self updateWindowStatusBarCapture]; +} + +- (void)updateWindowStatusBarCapture { + if (!self.window) return; + // 当以 window 的方式显示浮层时,状态栏交给 QMUIModalPresentationViewController 控制 + self.window.qmui_capturesStatusBarAppearance = self.qmui_prefersStatusBarHiddenBlock || self.qmui_preferredStatusBarStyleBlock; + if (self.window.qmui_capturesStatusBarAppearance) { + [self setNeedsStatusBarAppearanceUpdate]; } - return [super childViewControllerForStatusBarStyle]; } +// 当以 present 方式显示浮层时,状态栏允许由 contentViewController 控制,但 QMUIModalPresentationViewController 的 qmui_prefersStatusBarHiddenBlock/qmui_preferredStatusBarStyleBlock 优先级会更高 - (UIViewController *)childViewControllerForStatusBarHidden { - if (self.shownInPresentedMode) { + if (self.shownInPresentedMode && self.contentViewController) { return self.contentViewController; } return [super childViewControllerForStatusBarHidden]; } +- (UIViewController *)childViewControllerForStatusBarStyle { + if (self.shownInPresentedMode && self.contentViewController) { + return self.contentViewController; + } + return [super childViewControllerForStatusBarStyle]; +} + - (UIViewController *)childViewControllerForHomeIndicatorAutoHidden { if (self.shownInPresentedMode) { return self.contentViewController; @@ -632,7 +751,7 @@ - (UIViewController *)childViewControllerForHomeIndicatorAutoHidden { @implementation QMUIModalPresentationViewController (Manager) + (BOOL)isAnyModalPresentationViewControllerVisible { - for (UIWindow *window in [[UIApplication sharedApplication] windows]) { + for (UIWindow *window in UIApplication.sharedApplication.windows) { if ([window isKindOfClass:[QMUIModalPresentationWindow class]] && !window.hidden) { return YES; } @@ -644,7 +763,7 @@ + (BOOL)hideAllVisibleModalPresentationViewControllerIfCan { BOOL hideAllFinally = YES; - for (UIWindow *window in [[UIApplication sharedApplication] windows]) { + for (UIWindow *window in UIApplication.sharedApplication.windows) { if (![window isKindOfClass:[QMUIModalPresentationWindow class]]) { continue; } @@ -691,21 +810,40 @@ + (BOOL)hideAllVisibleModalPresentationViewControllerIfCan { @implementation QMUIModalPresentationWindow -- (void)layoutSubviews { - [super layoutSubviews]; - if (self.rootViewController) { - // https://github.com/Tencent/QMUI_iOS/issues/375 - UIView *rootView = self.rootViewController.view; - if (CGRectGetMinY(rootView.frame) > 0 && ![UIApplication sharedApplication].statusBarHidden && StatusBarHeight > CGRectGetMinY(rootView.frame)) { - rootView.frame = self.bounds; - } - } -} - @end @implementation UIViewController (QMUIModalPresentationViewController) QMUISynthesizeIdWeakProperty(qmui_modalPresentationViewController, setQmui_modalPresentationViewController) ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // present 方式显示的 modal,通过拦截 dismiss 方法来实现 shouldHide 的 delegate。注意以 window 方式显示的 modal,在 window.rootViewController = nil 时系统默认也会调用 dismiss,此时要通过 isShownInPresentedMode 区分开。 + OverrideImplementation([UIViewController class], @selector(dismissViewControllerAnimated:completion:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIViewController *selfObject, BOOL firstArgv, id secondArgv) { + + QMUIModalPresentationViewController *modal = nil; + if ([selfObject.presentedViewController isKindOfClass:QMUIModalPresentationViewController.class]) { + modal = (QMUIModalPresentationViewController *)selfObject.presentedViewController; + } else if ([selfObject isKindOfClass:QMUIModalPresentationViewController.class] && !selfObject.presentedViewController && selfObject.presentingViewController.presentedViewController == selfObject) { + modal = (QMUIModalPresentationViewController *)selfObject; + } + if ([modal.delegate respondsToSelector:@selector(shouldHideModalPresentationViewController:)] && modal.isShownInPresentedMode) { + BOOL shouldHide = [modal.delegate shouldHideModalPresentationViewController:modal]; + if (!shouldHide) { + return; + } + } + + // call super + void (*originSelectorIMP)(id, SEL, BOOL, id); + originSelectorIMP = (void (*)(id, SEL, BOOL, id))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); + }; + }); + }); +} + @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.h b/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.h index b76f4490..f84addaa 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIMoreOperationController.h @@ -53,7 +53,7 @@ NS_ASSUME_NONNULL_BEGIN @interface QMUIMoreOperationController : UIViewController @property(nullable, nonatomic, strong) UIColor *contentBackgroundColor UI_APPEARANCE_SELECTOR;// 面板上半部分(不包含取消按钮)背景色 -@property(nonatomic, assign) CGFloat contentEdgeMargin UI_APPEARANCE_SELECTOR;// 面板距离屏幕的上下左右间距 +@property(nonatomic, assign) UIEdgeInsets contentEdgeMargins UI_APPEARANCE_SELECTOR;// 面板距离屏幕的上下左右间距 @property(nonatomic, assign) CGFloat contentMaximumWidth UI_APPEARANCE_SELECTOR;// 面板的最大宽度 @property(nonatomic, assign) CGFloat contentCornerRadius UI_APPEARANCE_SELECTOR;// 面板的圆角大小,当值大于 0 时会设置 self.view.clipsToBounds = YES @property(nonatomic, assign) UIEdgeInsets contentPaddings UI_APPEARANCE_SELECTOR;// 面板内部的 padding,UIScrollView 会布局在除去 padding 之后的区域 @@ -79,7 +79,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nullable, nonatomic, weak) id delegate; @property(nullable, nonatomic, strong, readonly) UIView *contentView;// 放 UIScrollView 的容器,与 cancelButton 区分开 -@property(nullable, nonatomic, strong, readonly) NSArray *scrollViews;// 获取当前的所有 UIScrollView +@property(nullable, nonatomic, copy, readonly) NSArray *scrollViews;// 获取当前的所有 UIScrollView /// 取消按钮,如果不需要,则自行设置其 hidden 为 YES @property(nullable, nonatomic, strong, readonly) QMUIButton *cancelButton; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.m b/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.m index 2a1ff042..269b7a46 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIMoreOperationController.m @@ -21,6 +21,7 @@ #import "NSArray+QMUI.h" #import "UIScrollView+QMUI.h" #import "QMUILog.h" +#import "QMUIAppearance.h" static NSInteger const kQMUIMoreOperationItemViewTagOffset = 999; @@ -38,51 +39,44 @@ - (void)formatItemViewStyleWithMoreOperationController:(QMUIMoreOperationControl @implementation QMUIMoreOperationController (UIAppearance) -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self appearance]; - }); ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; } -static QMUIMoreOperationController *moreOperationViewControllerAppearance; -+ (nonnull instancetype)appearance { ++ (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - [self resetAppearance]; + [self initAppearance]; }); - return moreOperationViewControllerAppearance; } -+ (void)resetAppearance { - if (!moreOperationViewControllerAppearance) { - moreOperationViewControllerAppearance = [[QMUIMoreOperationController alloc] init]; - moreOperationViewControllerAppearance.contentBackgroundColor = UIColorWhite; - moreOperationViewControllerAppearance.contentEdgeMargin = 10; - moreOperationViewControllerAppearance.contentMaximumWidth = [QMUIHelper screenSizeFor55Inch].width - moreOperationViewControllerAppearance.contentEdgeMargin * 2; - moreOperationViewControllerAppearance.contentCornerRadius = 10; - moreOperationViewControllerAppearance.contentPaddings = UIEdgeInsetsMake(10, 0, 5, 0); - - moreOperationViewControllerAppearance.scrollViewSeparatorColor = UIColorMakeWithRGBA(0, 0, 0, .15f); - moreOperationViewControllerAppearance.scrollViewContentInsets = UIEdgeInsetsMake(14, 8, 14, 8); - - moreOperationViewControllerAppearance.itemBackgroundColor = UIColorClear; - moreOperationViewControllerAppearance.itemTitleColor = UIColorGrayDarken; - moreOperationViewControllerAppearance.itemTitleFont = UIFontMake(11); - moreOperationViewControllerAppearance.itemPaddingHorizontal = 16; - moreOperationViewControllerAppearance.itemTitleMarginTop = 9; - moreOperationViewControllerAppearance.itemMinimumMarginHorizontal = 0; - moreOperationViewControllerAppearance.automaticallyAdjustItemMargins = YES; - - moreOperationViewControllerAppearance.cancelButtonBackgroundColor = UIColorWhite; - moreOperationViewControllerAppearance.cancelButtonTitleColor = UIColorBlue; - moreOperationViewControllerAppearance.cancelButtonSeparatorColor = UIColorMakeWithRGBA(0, 0, 0, .15f); - moreOperationViewControllerAppearance.cancelButtonFont = UIFontBoldMake(16); - moreOperationViewControllerAppearance.cancelButtonHeight = 56.0; - moreOperationViewControllerAppearance.cancelButtonMarginTop = 0; - - moreOperationViewControllerAppearance.isExtendBottomLayout = NO; - } ++ (void)initAppearance { + QMUIMoreOperationController *moreOperationViewControllerAppearance = QMUIMoreOperationController.appearance; + moreOperationViewControllerAppearance.contentBackgroundColor = UIColorForBackground; + moreOperationViewControllerAppearance.contentEdgeMargins = UIEdgeInsetsMake(0, 10, 10, 10); + moreOperationViewControllerAppearance.contentMaximumWidth = [QMUIHelper screenSizeFor55Inch].width - UIEdgeInsetsGetHorizontalValue(moreOperationViewControllerAppearance.contentEdgeMargins); + moreOperationViewControllerAppearance.contentCornerRadius = 10; + moreOperationViewControllerAppearance.contentPaddings = UIEdgeInsetsMake(10, 0, 5, 0); + + moreOperationViewControllerAppearance.scrollViewSeparatorColor = UIColorMakeWithRGBA(0, 0, 0, .15f); + moreOperationViewControllerAppearance.scrollViewContentInsets = UIEdgeInsetsMake(14, 8, 14, 8); + + moreOperationViewControllerAppearance.itemBackgroundColor = UIColorClear; + moreOperationViewControllerAppearance.itemTitleColor = UIColorGrayDarken; + moreOperationViewControllerAppearance.itemTitleFont = UIFontMake(11); + moreOperationViewControllerAppearance.itemPaddingHorizontal = 16; + moreOperationViewControllerAppearance.itemTitleMarginTop = 9; + moreOperationViewControllerAppearance.itemMinimumMarginHorizontal = 0; + moreOperationViewControllerAppearance.automaticallyAdjustItemMargins = YES; + + moreOperationViewControllerAppearance.cancelButtonBackgroundColor = UIColorForBackground; + moreOperationViewControllerAppearance.cancelButtonTitleColor = UIColorBlue; + moreOperationViewControllerAppearance.cancelButtonSeparatorColor = UIColorMakeWithRGBA(0, 0, 0, .15f); + moreOperationViewControllerAppearance.cancelButtonFont = UIFontBoldMake(16); + moreOperationViewControllerAppearance.cancelButtonHeight = 56.0; + moreOperationViewControllerAppearance.cancelButtonMarginTop = 0; + + moreOperationViewControllerAppearance.isExtendBottomLayout = NO; } @end @@ -116,58 +110,45 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { } - (void)didInitialize { - if (moreOperationViewControllerAppearance) { - self.contentBackgroundColor = [QMUIMoreOperationController appearance].contentBackgroundColor; - self.contentEdgeMargin = [QMUIMoreOperationController appearance].contentEdgeMargin; - self.contentMaximumWidth = [QMUIMoreOperationController appearance].contentMaximumWidth; - self.contentCornerRadius = [QMUIMoreOperationController appearance].contentCornerRadius; - self.contentPaddings = [QMUIMoreOperationController appearance].contentPaddings; - - self.scrollViewSeparatorColor = [QMUIMoreOperationController appearance].scrollViewSeparatorColor; - self.scrollViewContentInsets = [QMUIMoreOperationController appearance].scrollViewContentInsets; - - self.itemBackgroundColor = [QMUIMoreOperationController appearance].itemBackgroundColor; - self.itemTitleColor = [QMUIMoreOperationController appearance].itemTitleColor; - self.itemTitleFont = [QMUIMoreOperationController appearance].itemTitleFont; - self.itemPaddingHorizontal = [QMUIMoreOperationController appearance].itemPaddingHorizontal; - self.itemTitleMarginTop = [QMUIMoreOperationController appearance].itemTitleMarginTop; - self.itemMinimumMarginHorizontal = [QMUIMoreOperationController appearance].itemMinimumMarginHorizontal; - self.automaticallyAdjustItemMargins = [QMUIMoreOperationController appearance].automaticallyAdjustItemMargins; - - self.cancelButtonBackgroundColor = [QMUIMoreOperationController appearance].cancelButtonBackgroundColor; - self.cancelButtonTitleColor = [QMUIMoreOperationController appearance].cancelButtonTitleColor; - self.cancelButtonSeparatorColor = [QMUIMoreOperationController appearance].cancelButtonSeparatorColor; - self.cancelButtonFont = [QMUIMoreOperationController appearance].cancelButtonFont; - self.cancelButtonHeight = [QMUIMoreOperationController appearance].cancelButtonHeight; - self.cancelButtonMarginTop = [QMUIMoreOperationController appearance].cancelButtonMarginTop; - - self.isExtendBottomLayout = [QMUIMoreOperationController appearance].isExtendBottomLayout; - - self.mutableScrollViews = [[NSMutableArray alloc] init]; - self.mutableItems = [[NSMutableArray alloc] init]; - } + [self qmui_applyAppearance]; - [self loadViewIfNeeded]; + self.mutableScrollViews = [[NSMutableArray alloc] init]; + self.mutableItems = [[NSMutableArray alloc] init]; +} + +#pragma mark - Getters & Setters + +@synthesize contentView = _contentView; +- (UIView *)contentView { + if (!_contentView) { + _contentView = [[UIView alloc] init]; + _contentView.backgroundColor = self.contentBackgroundColor; + } + return _contentView; +} + +@synthesize cancelButton = _cancelButton; +- (QMUIButton *)cancelButton { + if (!_cancelButton) { + _cancelButton = [[QMUIButton alloc] init]; + _cancelButton.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; + _cancelButton.adjustsButtonWhenHighlighted = NO; + _cancelButton.titleLabel.font = self.cancelButtonFont; + _cancelButton.backgroundColor = self.cancelButtonBackgroundColor; + [_cancelButton setTitle:@"取消" forState:UIControlStateNormal]; + [_cancelButton setTitleColor:self.cancelButtonTitleColor forState:UIControlStateNormal]; + [_cancelButton setTitleColor:[self.cancelButtonTitleColor colorWithAlphaComponent:ButtonHighlightedAlpha] forState:UIControlStateHighlighted]; + _cancelButton.qmui_borderPosition = self.cancelButtonMarginTop > 0 ? QMUIViewBorderPositionNone : QMUIViewBorderPositionTop; + _cancelButton.qmui_borderColor = self.cancelButtonSeparatorColor; + [_cancelButton addTarget:self action:@selector(handleCancelButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + } + return _cancelButton; } - (void)viewDidLoad { [super viewDidLoad]; - _contentView = [[UIView alloc] init]; - self.contentView.backgroundColor = self.contentBackgroundColor; [self.view addSubview:self.contentView]; - - _cancelButton = [[QMUIButton alloc] init]; - self.cancelButton.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; - self.cancelButton.adjustsButtonWhenHighlighted = NO; - self.cancelButton.titleLabel.font = self.cancelButtonFont; - self.cancelButton.backgroundColor = self.cancelButtonBackgroundColor; - [self.cancelButton setTitle:@"取消" forState:UIControlStateNormal]; - [self.cancelButton setTitleColor:self.cancelButtonTitleColor forState:UIControlStateNormal]; - [self.cancelButton setTitleColor:[self.cancelButtonTitleColor colorWithAlphaComponent:ButtonHighlightedAlpha] forState:UIControlStateHighlighted]; - self.cancelButton.qmui_borderPosition = QMUIViewBorderPositionBottom; - self.cancelButton.qmui_borderColor = self.cancelButtonSeparatorColor; - [self.cancelButton addTarget:self action:@selector(handleCancelButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:self.cancelButton]; self.extendLayer = [CALayer layer]; @@ -211,10 +192,10 @@ - (void)viewDidLayoutSubviews { [self.mutableScrollViews enumerateObjectsUsingBlock:^(UIScrollView * _Nonnull scrollView, NSUInteger idx, BOOL * _Nonnull stop) { scrollView.frame = CGRectMake(self.contentPaddings.left, layoutY, contentWidth, CGRectGetHeight(scrollView.frame)); - // 要保护 safeAreaInsets 的区域,而这里不使用 scrollView.qmui_safeAreaInsets 是因为此时 scrollView 的 safeAreaInsets 仍然为 0,但 scrollView.superview.safeAreaInsets 已经正确了,所以使用 scrollView.superview 也即 self.view 的 + // 要保护 safeAreaInsets 的区域,而这里不使用 scrollView.safeAreaInsets 是因为此时 scrollView 的 safeAreaInsets 仍然为 0,但 scrollView.superview.safeAreaInsets 已经正确了,所以使用 scrollView.superview 也即 self.view 的 // 底部的 insets 暂不考虑 -// UIEdgeInsets scrollViewSafeAreaInsets = scrollView.qmui_safeAreaInsets; - UIEdgeInsets scrollViewSafeAreaInsets = UIEdgeInsetsMake(fmax(self.view.qmui_safeAreaInsets.top - scrollView.qmui_top, 0), fmax(self.view.qmui_safeAreaInsets.left - scrollView.qmui_left, 0), 0, fmax(self.view.qmui_safeAreaInsets.right - (self.view.qmui_width - scrollView.qmui_right), 0)); +// UIEdgeInsets scrollViewSafeAreaInsets = scrollView.safeAreaInsets; + UIEdgeInsets scrollViewSafeAreaInsets = UIEdgeInsetsMake(fmax(self.view.safeAreaInsets.top - scrollView.qmui_top, 0), fmax(self.view.safeAreaInsets.left - scrollView.qmui_left, 0), 0, fmax(self.view.safeAreaInsets.right - (self.view.qmui_width - scrollView.qmui_right), 0)); NSArray *itemSection = self.mutableItems[idx]; QMUIMoreOperationItemView *exampleItemView = itemSection.firstObject; @@ -262,12 +243,12 @@ - (void)showFromBottom { QMUIModalPresentationViewController *modalPresentationViewController = [[QMUIModalPresentationViewController alloc] init]; modalPresentationViewController.delegate = self; modalPresentationViewController.maximumContentViewWidth = self.contentMaximumWidth; - modalPresentationViewController.contentViewMargins = UIEdgeInsetsMake(self.contentEdgeMargin, self.contentEdgeMargin, self.contentEdgeMargin, self.contentEdgeMargin); + modalPresentationViewController.contentViewMargins = self.contentEdgeMargins; modalPresentationViewController.contentViewController = self; __weak __typeof(modalPresentationViewController)weakModalController = modalPresentationViewController; modalPresentationViewController.layoutBlock = ^(CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewDefaultFrame) { - weakModalController.contentView.frame = CGRectSetY(contentViewDefaultFrame, CGRectGetHeight(containerBounds) - weakModalController.contentViewMargins.bottom - CGRectGetHeight(contentViewDefaultFrame) - weakModalController.view.qmui_safeAreaInsets.bottom); + weakModalController.contentView.qmui_frameApplyTransform = CGRectSetY(contentViewDefaultFrame, CGRectGetHeight(containerBounds) - weakModalController.contentViewMargins.bottom - CGRectGetHeight(contentViewDefaultFrame) - weakModalController.view.safeAreaInsets.bottom); }; modalPresentationViewController.showingAnimation = ^(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewFrame, void(^completion)(BOOL finished)) { @@ -440,9 +421,7 @@ - (UIScrollView *)generateScrollViewWithIndex:(NSInteger)index { scrollView.qmui_borderColor = self.scrollViewSeparatorColor; scrollView.qmui_borderPosition = (self.scrollViewSeparatorColor && index != 0) ? QMUIViewBorderPositionTop : QMUIViewBorderPositionNone; scrollView.scrollsToTop = NO; - if (@available(iOS 11, *)) { - scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } + scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; scrollView.contentInset = self.scrollViewContentInsets; [scrollView qmui_scrollToTopForce:YES animated:NO]; return scrollView; @@ -477,7 +456,7 @@ - (void)handleItemViewEvent:(QMUIMoreOperationItemView *)itemView { - (void)setContentBackgroundColor:(UIColor *)contentBackgroundColor { _contentBackgroundColor = contentBackgroundColor; - self.contentView.backgroundColor = contentBackgroundColor; + _contentView.backgroundColor = contentBackgroundColor; } - (void)setScrollViewSeparatorColor:(UIColor *)scrollViewSeparatorColor { @@ -497,21 +476,21 @@ - (void)setScrollViewContentInsets:(UIEdgeInsets)scrollViewContentInsets { - (void)setCancelButtonBackgroundColor:(UIColor *)cancelButtonBackgroundColor { _cancelButtonBackgroundColor = cancelButtonBackgroundColor; - self.cancelButton.backgroundColor = cancelButtonBackgroundColor; + _cancelButton.backgroundColor = cancelButtonBackgroundColor; [self updateExtendLayerAppearance]; } - (void)setCancelButtonTitleColor:(UIColor *)cancelButtonTitleColor { _cancelButtonTitleColor = cancelButtonTitleColor; - if (self.cancelButton) { - [self.cancelButton setTitleColor:cancelButtonTitleColor forState:UIControlStateNormal]; - [self.cancelButton setTitleColor:[cancelButtonTitleColor colorWithAlphaComponent:ButtonHighlightedAlpha] forState:UIControlStateHighlighted]; + if (_cancelButton) { + [_cancelButton setTitleColor:cancelButtonTitleColor forState:UIControlStateNormal]; + [_cancelButton setTitleColor:[cancelButtonTitleColor colorWithAlphaComponent:ButtonHighlightedAlpha] forState:UIControlStateHighlighted]; } } - (void)setCancelButtonSeparatorColor:(UIColor *)cancelButtonSeparatorColor { _cancelButtonSeparatorColor = cancelButtonSeparatorColor; - self.cancelButton.qmui_borderColor = cancelButtonSeparatorColor; + _cancelButton.qmui_borderColor = cancelButtonSeparatorColor; } - (void)setItemBackgroundColor:(UIColor *)itemBackgroundColor { @@ -561,8 +540,8 @@ - (void)setAutomaticallyAdjustItemMargins:(BOOL)automaticallyAdjustItemMargins { - (void)setCancelButtonFont:(UIFont *)cancelButtonFont { _cancelButtonFont = cancelButtonFont; - self.cancelButton.titleLabel.font = cancelButtonFont; - [self.cancelButton setNeedsLayout]; + _cancelButton.titleLabel.font = cancelButtonFont; + [_cancelButton setNeedsLayout]; } - (void)setContentCornerRadius:(CGFloat)contentCornerRadius { @@ -572,7 +551,7 @@ - (void)setContentCornerRadius:(CGFloat)contentCornerRadius { - (void)setCancelButtonMarginTop:(CGFloat)cancelButtonMarginTop { _cancelButtonMarginTop = cancelButtonMarginTop; - self.cancelButton.qmui_borderPosition = cancelButtonMarginTop > 0 ? QMUIViewBorderPositionNone : QMUIViewBorderPositionTop; + _cancelButton.qmui_borderPosition = cancelButtonMarginTop > 0 ? QMUIViewBorderPositionNone : QMUIViewBorderPositionTop; [self updateCornerRadius]; [self setViewNeedsLayoutIfLoaded]; } @@ -602,16 +581,20 @@ - (void)updateExtendLayerAppearance { - (void)updateCornerRadius { if (self.cancelButtonMarginTop > 0) { - self.view.layer.cornerRadius = 0; - self.view.clipsToBounds = NO; + if (self.isViewLoaded) { + self.view.layer.cornerRadius = 0; + self.view.clipsToBounds = NO; + } - self.contentView.layer.cornerRadius = self.contentCornerRadius; - self.cancelButton.layer.cornerRadius = self.contentCornerRadius; + _contentView.layer.cornerRadius = self.contentCornerRadius; + _cancelButton.layer.cornerRadius = self.contentCornerRadius; } else { - self.view.layer.cornerRadius = self.contentCornerRadius; - self.view.clipsToBounds = self.view.layer.cornerRadius > 0;// 有圆角才需要 clip - self.contentView.layer.cornerRadius = 0; - self.cancelButton.layer.cornerRadius = 0; + if (self.isViewLoaded) { + self.view.layer.cornerRadius = self.contentCornerRadius; + self.view.clipsToBounds = self.view.layer.cornerRadius > 0;// 有圆角才需要 clip + } + _contentView.layer.cornerRadius = 0; + _cancelButton.layer.cornerRadius = 0; } } @@ -667,7 +650,7 @@ @implementation QMUIMoreOperationItemView @dynamic tag; + (instancetype)itemViewWithImage:(UIImage *)image selectedImage:(UIImage *)selectedImage title:(NSString *)title selectedTitle:(NSString *)selectedTitle handler:(void (^)(QMUIMoreOperationController *, QMUIMoreOperationItemView *))handler { - QMUIMoreOperationItemView *itemView = [[QMUIMoreOperationItemView alloc] init]; + QMUIMoreOperationItemView *itemView = [[self alloc] init]; [itemView setImage:image forState:UIControlStateNormal]; [itemView setImage:selectedImage forState:UIControlStateSelected]; [itemView setImage:selectedImage forState:UIControlStateHighlighted|UIControlStateSelected]; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.h b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.h index 58e760d3..e891a641 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSObject+MultipleDelegates.h @@ -27,6 +27,8 @@ * [self qmui_registerDelegateSelector:@selector(abcDelegate)]; * self.abcDelegate = delegateA; * self.abcDelegate = delegateB; + * + * @warning 不支持 self.delegate = self 的写法,会引发死循环,有这种需求的场景建议在 self 内部创建一个对象专门用于 delegate 的响应,参考 _QMUITextViewDelegator。 */ @interface NSObject (QMUIMultipleDelegates) @@ -40,10 +42,3 @@ - (void)qmui_removeDelegate:(id)delegate; @end - -@interface NSObject (QMUIMultipleDelegates_Private) - -/// 内部使用,用于标志“xxx.delegate = xxx”的情况,具体请看 https://github.com/Tencent/QMUI_iOS/issues/346 -@property(nonatomic, assign) BOOL qmui_delegatesSelf; - -@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m index f18af983..9b4b9531 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSObject+MultipleDelegates.m @@ -17,7 +17,7 @@ #import "QMUIMultipleDelegates.h" #import "QMUICore.h" #import "NSPointerArray+QMUI.h" -#import +#import "NSString+QMUI.h" @interface NSObject () @@ -28,8 +28,6 @@ @implementation NSObject (QMUIMultipleDelegates) QMUISynthesizeIdStrongProperty(qmuimd_delegates, setQmuimd_delegates) -static NSMutableSet *qmui_methodsReplacedClasses; - static char kAssociatedObjectKey_qmuiMultipleDelegatesEnabled; - (void)setQmui_multipleDelegatesEnabled:(BOOL)qmui_multipleDelegatesEnabled { objc_setAssociatedObject(self, &kAssociatedObjectKey_qmuiMultipleDelegatesEnabled, @(qmui_multipleDelegatesEnabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC); @@ -54,39 +52,16 @@ - (void)qmui_registerDelegateSelector:(SEL)getter { } Class targetClass = [self class]; - SEL originDelegateSetter = [self originSetterWithGetter:getter]; + SEL originDelegateSetter = setterWithGetter(getter); SEL newDelegateSetter = [self newSetterWithGetter:getter]; Method originMethod = class_getInstanceMethod(targetClass, originDelegateSetter); if (!originMethod) { return; } - // 为这个 selector 创建一个 QMUIMultipleDelegates 容器 NSString *delegateGetterKey = NSStringFromSelector(getter); - if (!self.qmuimd_delegates[delegateGetterKey]) { - objc_property_t prop = class_getProperty(self.class, delegateGetterKey.UTF8String); - QMUIPropertyDescriptor *property = [QMUIPropertyDescriptor descriptorWithProperty:prop]; - if (property.isStrong) { - // strong property - QMUIMultipleDelegates *strongDelegates = [QMUIMultipleDelegates strongDelegates]; - strongDelegates.parentObject = self; - self.qmuimd_delegates[delegateGetterKey] = strongDelegates; - } else { - // weak property - QMUIMultipleDelegates *weakDelegates = [QMUIMultipleDelegates weakDelegates]; - weakDelegates.parentObject = self; - self.qmuimd_delegates[delegateGetterKey] = weakDelegates; - } - } - // 避免为某个 class 重复替换同一个方法的实现 - if (!qmui_methodsReplacedClasses) { - qmui_methodsReplacedClasses = [NSMutableSet set]; - } - NSString *classAndMethodIdentifier = [NSString stringWithFormat:@"%@-%@", NSStringFromClass(targetClass), delegateGetterKey]; - if (![qmui_methodsReplacedClasses containsObject:classAndMethodIdentifier]) { - [qmui_methodsReplacedClasses addObject:classAndMethodIdentifier]; - + [QMUIHelper executeBlock:^{ IMP originIMP = method_getImplementation(originMethod); void (*originSelectorIMP)(id, SEL, id); originSelectorIMP = (void (*)(id, SEL, id))originIMP; @@ -99,24 +74,38 @@ - (void)qmui_registerDelegateSelector:(SEL)getter { return; } + // 为这个 selector 创建一个 QMUIMultipleDelegates 容器 QMUIMultipleDelegates *delegates = selfObject.qmuimd_delegates[delegateGetterKey]; - if (!aDelegate) { // 对应 setDelegate:nil,表示清理所有的 delegate - [delegates removeAllDelegates]; - selfObject.qmui_delegatesSelf = NO; - // 只要 qmui_multipleDelegatesEnabled 开启,就会保证 delegate 一直是 delegates,所以不去调用系统默认的 set nil - // originSelectorIMP(selfObject, originDelegateSetter, nil); + if (delegates) { + [delegates removeAllDelegates]; + [selfObject.qmuimd_delegates removeObjectForKey:delegateGetterKey]; + } + // 必须要清空,否则遇到像 tableView:cellForRowAtIndexPath: 这种“要求返回值不能为 nil” 的场景就会中 assert + // https://github.com/Tencent/QMUI_iOS/issues/1411 + originSelectorIMP(selfObject, originDelegateSetter, nil); return; } + if (!delegates) { + objc_property_t prop = class_getProperty(selfObject.class, delegateGetterKey.UTF8String); + QMUIPropertyDescriptor *property = [QMUIPropertyDescriptor descriptorWithProperty:prop]; + if (property.isStrong) { + // strong property + delegates = [QMUIMultipleDelegates strongDelegates]; + } else { + // weak property + delegates = [QMUIMultipleDelegates weakDelegates]; + } + delegates.parentObject = selfObject; + selfObject.qmuimd_delegates[delegateGetterKey] = delegates; + } + if (aDelegate != delegates) {// 过滤掉容器自身,避免把 delegates 传进去 delegates 里,导致死循环 [delegates addDelegate:aDelegate]; } - // 将类似 textView.delegate = textView 的情况标志起来,避免产生循环调用 https://github.com/Tencent/QMUI_iOS/issues/346 - selfObject.qmui_delegatesSelf = [delegates.delegates qmui_containsPointer:(__bridge void * _Nullable)(selfObject)]; - originSelectorIMP(selfObject, originDelegateSetter, nil);// 先置为 nil 再设置 delegates,从而避免这个问题 https://github.com/Tencent/QMUI_iOS/issues/305 originSelectorIMP(selfObject, originDelegateSetter, delegates);// 不管外面将什么 object 传给 setDelegate:,最终实际上传进去的都是 QMUIMultipleDelegates 容器 @@ -125,7 +114,7 @@ - (void)qmui_registerDelegateSelector:(SEL)getter { Method newMethod = class_getInstanceMethod(targetClass, newDelegateSetter); method_exchangeImplementations(originMethod, newMethod); } - } + } oncePerIdentifier:[NSString stringWithFormat:@"MultipleDelegates %@-%@", NSStringFromClass(targetClass), NSStringFromSelector(getter)]]; // 如果原来已经有 delegate,则将其加到新建的容器里 // @see https://github.com/Tencent/QMUI_iOS/issues/378 @@ -138,7 +127,7 @@ - (void)qmui_registerDelegateSelector:(SEL)getter { } - (void)qmui_removeDelegate:(id)delegate { - if (!self.qmui_multipleDelegatesEnabled) { + if (!self.qmuimd_delegates) { return; } NSMutableArray *delegateGetters = [[NSMutableArray alloc] init]; @@ -164,26 +153,9 @@ - (void)refreshDelegateWithGetter:(SEL)getter { EndIgnorePerformSelectorLeaksWarning } -// 根据 delegate property 的 getter,得到它对应的 setter -- (SEL)originSetterWithGetter:(SEL)getter { - NSString *getterString = NSStringFromSelector(getter); - NSMutableString *setterString = [[NSMutableString alloc] initWithString:@"set"]; - [setterString appendString:[getterString substringToIndex:1].uppercaseString]; - [setterString appendString:[getterString substringFromIndex:1]]; - [setterString appendString:@":"]; - SEL setter = NSSelectorFromString(setterString); - return setter; -} - // 根据 delegate property 的 getter,得到 QMUIMultipleDelegates 为它的 setter 创建的新 setter 方法,最终交换原方法,因此利用这个方法返回的 SEL,可以调用到原来的 delegate property setter 的实现 - (SEL)newSetterWithGetter:(SEL)getter { - return NSSelectorFromString([NSString stringWithFormat:@"qmuimd_%@", NSStringFromSelector([self originSetterWithGetter:getter])]); + return NSSelectorFromString([NSString stringWithFormat:@"qmuimd_%@", NSStringFromSelector(setterWithGetter(getter))]); } @end - -@implementation NSObject (QMUIMultipleDelegates_Private) - -QMUISynthesizeBOOLProperty(qmui_delegatesSelf, setQmui_delegatesSelf) - -@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.h b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.h index a8c2b130..c6c22146 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIMultipleDelegates.h @@ -30,4 +30,5 @@ - (BOOL)removeDelegate:(id)delegate; - (void)removeAllDelegates; - (BOOL)containsDelegate:(id)delegate; + @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m index 74e7bceb..d825d4fd 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIMultipleDelegates.m @@ -15,30 +15,53 @@ #import "QMUIMultipleDelegates.h" #import "NSPointerArray+QMUI.h" -#import +#import "NSMethodSignature+QMUI.h" +#import "NSObject+QMUI.h" +#import "QMUICore.h" @interface QMUIMultipleDelegates () @property(nonatomic, strong, readwrite) NSPointerArray *delegates; +@property(nonatomic, strong) NSInvocation *forwardingInvocation; +@property(nonatomic, assign) SEL inquiringSelector; @end @implementation QMUIMultipleDelegates + (instancetype)weakDelegates { - QMUIMultipleDelegates *delegates = [[QMUIMultipleDelegates alloc] init]; + QMUIMultipleDelegates *delegates = [[self alloc] init]; delegates.delegates = [NSPointerArray weakObjectsPointerArray]; return delegates; } + (instancetype)strongDelegates { - QMUIMultipleDelegates *delegates = [[QMUIMultipleDelegates alloc] init]; + QMUIMultipleDelegates *delegates = [[self alloc] init]; delegates.delegates = [NSPointerArray strongObjectsPointerArray]; return delegates; } +- (void)resetClassNameIfNeeded { + if ([self.parentObject isKindOfClass:CALayer.class] || [self.parentObject isKindOfClass:CAAnimation.class]) { + // CALayer 和 CAAnimation 会缓存同一个 delegate class 的 respondsToSelector: 结果,但是在 multipleDelegates 的设计下,可能存在当前的 delegate 无法响应某个 selector,而后添加了可以响应的 delegate,系统这个缓存机制仍会认为无法响应,所以每次添加新的 delegate 都要设置与之前不同的 className + // 这里设置一个 QMUIMultipleDelegates 的 subClass,其 className 由所有 delegate className 拼接而成。 + NSMutableString *className = [NSMutableString stringWithString:NSStringFromClass(QMUIMultipleDelegates.class)]; + [self.delegates.allObjects enumerateObjectsUsingBlock:^(id _Nonnull delegate, NSUInteger idx, BOOL * _Nonnull stop) { + NSString *delegateClassName = NSStringFromClass(object_getClass(delegate)); + [className appendFormat:@"_%@", delegateClassName]; + }]; + Class class = NSClassFromString(className); + if (!class) { + class = objc_allocateClassPair(QMUIMultipleDelegates.class, className.UTF8String, 0); + objc_registerClassPair(class); + } + object_setClass(self, class); + } +} + - (void)addDelegate:(id)delegate { if (![self containsDelegate:delegate] && delegate != self) { [self.delegates addPointer:(__bridge void *)delegate]; + [self resetClassNameIfNeeded]; } } @@ -62,11 +85,7 @@ - (BOOL)containsDelegate:(id)delegate { } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { - NSMethodSignature *result = [super methodSignatureForSelector:aSelector]; - if (result) { - return result; - } - + NSMethodSignature *result = nil; NSPointerArray *delegates = [self.delegates copy]; for (id delegate in delegates) { result = [delegate methodSignatureForSelector:aSelector]; @@ -74,28 +93,53 @@ - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return result; } } - // https://github.com/facebookarchive/AsyncDisplayKit/pull/1562 - // Unfortunately, in order to get this object to work properly, the use of a method which creates an NSMethodSignature - // from a C string. -methodSignatureForSelector is called when a compiled definition for the selector cannot be found. - // This is the place where we have to create our own dud NSMethodSignature. This is necessary because if this method - // returns nil, a selector not found exception is raised. The string argument to -signatureWithObjCTypes: outlines - // the return type and arguments to the message. To return a dud NSMethodSignature, pretty much any signature will - // suffice. Since the -forwardInvocation call will do nothing if the delegate does not respond to the selector, - // the dud NSMethodSignature simply gets us around the exception. - return [NSMethodSignature signatureWithObjCTypes:"@^v^c"]; + + return NSMethodSignature.qmui_avoidExceptionSignature; } - (void)forwardInvocation:(NSInvocation *)anInvocation { SEL selector = anInvocation.selector; - NSPointerArray *delegates = [self.delegates copy]; + + // RAC 那边会把相同的 invocation 传回来 QMUIMultipleDelegates,引发死循环,所以这里做了个屏蔽 + // https://github.com/Tencent/QMUI_iOS/issues/970 + if (self.forwardingInvocation.selector != NULL && self.forwardingInvocation.selector == selector) { + NSUInteger returnLength = anInvocation.methodSignature.methodReturnLength; + if (returnLength) { + void *buffer = (void *)malloc(returnLength); + [self.forwardingInvocation getReturnValue:buffer]; + [anInvocation setReturnValue:buffer]; + free(buffer); + } + return; + } + + NSPointerArray *delegates = self.delegates.copy; for (id delegate in delegates) { if ([delegate respondsToSelector:selector]) { + // 当前 delegate 的实现可能再次调用原始 delegate 的实现,如果原始 delegate 是 QMUIMultipleDelegates 就会造成死循环,所以要做 2 事: + // 1、检测到循环就打破 + // 2、但是检测到循环时,新生成的 anInvocation 默认没有 returnValue,需要用上一次循环之前的结果 + self.forwardingInvocation = anInvocation; [anInvocation invokeWithTarget:delegate]; } } + + self.forwardingInvocation = nil; } - (BOOL)respondsToSelector:(SEL)aSelector { + + if (self.inquiringSelector == aSelector) { + /** + 这个判断是为了避免类似 RACDelegateProxy 的处理导致的死循环: + RACDelegateProxy 会做以下事情: + 1.保存之前的代理 + 2.把对象代理修改为 RACDelegateProxy + 由于 QMUIMultipleDelegates 会拦截操作 2,保持原始代理一直是 QMUIMultipleDelegates 不被修改,同时把 RACDelegateProxy 添加到 delegates,而 RACDelegateProxy 操作 1 又保存了 QMUIMultipleDelegates 实例,当对其调用 respondsToSelector 时,又会转发到 QMUIMultipleDelegates 造成死循环,所以要做这个保护。 + */ + return NO; + } + if ([super respondsToSelector:aSelector]) { return YES; } @@ -107,19 +151,84 @@ - (BOOL)respondsToSelector:(SEL)aSelector { } // 对 QMUIMultipleDelegates 额外处理的解释在这里:https://github.com/Tencent/QMUI_iOS/issues/357 - BOOL delegateCanRespondToSelector = [delegate isKindOfClass:self.class] ? [delegate respondsToSelector:aSelector] : class_respondsToSelector(((NSObject *)delegate).class, aSelector); - - // 判断 qmui_delegatesSelf 是为了解决这个 issue:https://github.com/Tencent/QMUI_iOS/issues/346 - BOOL isDelegateSelf = ((NSObject *)delegate).qmui_delegatesSelf; - if (delegateCanRespondToSelector && !isDelegateSelf) { + BOOL delegateCanRespondToSelector; + if ([delegate isProxy] || [delegate isKindOfClass:QMUIMultipleDelegates.class]) { + self.inquiringSelector = aSelector; + delegateCanRespondToSelector = [delegate respondsToSelector:aSelector]; + self.inquiringSelector = NULL; + } else { + delegateCanRespondToSelector = class_respondsToSelector(object_getClass(delegate), aSelector); + } + if (delegateCanRespondToSelector) { return YES; } } return NO; } +#pragma mark - Overrides + +- (BOOL)isProxy { + return YES; +} + +- (BOOL)isKindOfClass:(Class)aClass { + BOOL result = [super isKindOfClass:aClass]; + if (result) return YES; + + NSPointerArray *delegates = [self.delegates copy]; + for (id delegate in delegates) { + if ([delegate isKindOfClass:aClass]) return YES; + } + + return NO; +} + +- (BOOL)isMemberOfClass:(Class)aClass { + BOOL result = [super isMemberOfClass:aClass]; + if (result) return YES; + + NSPointerArray *delegates = [self.delegates copy]; + for (id delegate in delegates) { + if ([delegate isMemberOfClass:aClass]) return YES; + } + + return NO; +} + +- (BOOL)conformsToProtocol:(Protocol *)aProtocol { + BOOL result = [super conformsToProtocol:aProtocol]; + if (result) return YES; + + NSPointerArray *delegates = [self.delegates copy]; + for (id delegate in delegates) { + if ([delegate conformsToProtocol:aProtocol]) return YES; + } + + return NO; +} + - (NSString *)description { return [NSString stringWithFormat:@"%@, parentObject is %@, %@", [super description], self.parentObject, self.delegates]; } +- (id)valueForKey:(NSString *)key { + NSPointerArray *delegates = [self.delegates copy]; + for (id delegate in delegates) { + if ([delegate qmui_canGetValueForKey:key]) { + return [delegate valueForKey:key]; + } + } + return [super valueForKey:key]; +} + +- (void)setValue:(id)value forKey:(NSString *)key { + NSPointerArray *delegates = [self.delegates copy]; + for (id delegate in delegates) { + if ([delegate qmui_canSetValueForKey:key]) { + [delegate setValue:value forKey:key]; + } + } +} + @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.h b/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.h index fae3fd9c..180f9163 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUINavigationTitleView.h @@ -54,7 +54,9 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) { /** - * 可作为navgationItem.titleView 的标题控件。 + * 可作为 UIViewController 顶部导航栏里的标题控件,通过 self.navigationItem.titleView 来设置。当调用 -[UIViewController setTitle:] 或 -[UINavigationItem setTitle:] 时,会自动更新 QMUINavigationTitleView 的内容。 + * + * 也可以当成单独的组件,脱离 UIViewController 使用,就跟普通组件一样。 * * 支持主副标题,且可控制主副标题的布局方式(水平或垂直);支持在左边显示loading,在右边显示accessoryView(如箭头)。 * @@ -70,6 +72,7 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) { @property(nonatomic, weak) id delegate; @property(nonatomic, assign) QMUINavigationTitleViewStyle style; @property(nonatomic, assign, getter=isActive) BOOL active; +@property(nonatomic, assign) UIEdgeInsets padding UI_APPEARANCE_SELECTOR; @property(nonatomic, assign) CGFloat maximumWidth UI_APPEARANCE_SELECTOR; #pragma mark - Titles @@ -80,6 +83,16 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) { @property(nonatomic, strong, readonly) UILabel *subtitleLabel; @property(nonatomic, copy) NSString *subtitle; +/// 当 tintColor 发生变化时是否要自动把 titleLabel、subtitleLabel、loadingView 的颜色也更新为 tintColor 的色值,默认为 YES,如果你自己修改了 titleLabel、subtitleLabel、loadingView 的颜色,需要把这个值置为 NO +@property(nonatomic, assign) BOOL adjustsSubviewsTintColorAutomatically UI_APPEARANCE_SELECTOR; + +/** + * 是否自动调整 highlighted 时的样式,默认为YES。
+ * 当值为 YES 时,标题 highlighted 时会改变自身的 alpha 属性为 UIControlHighlightedAlpha + * 适用于比如说整个 titleView 不需要接受点击,但 accessoryView 需要接受点击,此时就应该 titleView.userInteractionEnabled = YES、titleView.adjustsSubviewsWhenHighlighted = NO + */ +@property(nonatomic, assign) BOOL adjustsSubviewsWhenHighlighted; + /// 水平布局下的标题字体,默认为 NavBarTitleFont @property(nonatomic, strong) UIFont *horizontalTitleFont UI_APPEARANCE_SELECTOR; @@ -115,7 +128,7 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) { @property(nonatomic, assign) BOOL loadingViewHidden; /* - * 如果为YES则title居中,loading放在title的左边,title右边有一个跟左边loading一样大的占位空间;如果为NO,loading和title整体居中。默认值为YES。 + * 如果为YES则title居中,loading放在title的左边,title右边有一个跟左边loading一样大的占位空间(目的是为了让切换 loading 时文字不跳动);如果为NO,loading和title整体居中。默认值为YES。 */ @property(nonatomic, assign) BOOL needsLoadingPlaceholderSpace; @@ -170,3 +183,10 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) { - (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style; @end + +@interface UIView (QMUINavigationTitleView) + +/// 标记当前 view 是用于自定义的导航栏标题,QMUI 可以帮你自动处理系统的一些布局 bug,并且保证 pop 时导航栏标题颜色不会被前一个界面影响。 +/// 对于 QMUINavigationTitleView 而言默认值为 YES,其他 UIView 默认值为 NO。 +@property(nonatomic, assign) BOOL qmui_useAsNavigationTitleView; +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.m b/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.m index 8e225969..fb4a4bcd 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUINavigationTitleView.m @@ -19,59 +19,14 @@ #import "UIImage+QMUI.h" #import "UILabel+QMUI.h" #import "UIActivityIndicatorView+QMUI.h" +#import "UIViewController+QMUI.h" #import "UIView+QMUI.h" - -@interface UINavigationBar (TitleView) - -@end - -@implementation UINavigationBar (TitleView) - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - OverrideImplementation([UINavigationBar class], @selector(layoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UINavigationBar *selfObject) { - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - QMUINavigationTitleView *titleView = (QMUINavigationTitleView *)selfObject.topItem.titleView; - - if ([titleView isKindOfClass:[QMUINavigationTitleView class]]) { - CGFloat titleViewMaximumWidth = CGRectGetWidth(titleView.bounds);// 初始状态下titleView会被设置为UINavigationBar允许的最大宽度 - CGSize titleViewSize = [titleView sizeThatFits:CGSizeMake(titleViewMaximumWidth, CGFLOAT_MAX)]; - titleViewSize.height = ceil(titleViewSize.height);// titleView的高度如果非pt整数,会导致计算出来的y值时多时少,所以干脆做一下pt取整,这个策略不要改,改了要重新测试push过程中titleView是否会跳动 - - // 当在UINavigationBar里使用自定义的titleView时,就算titleView的sizeThatFits:返回正确的高度,navigationBar也不会帮你设置高度(但会帮你设置宽度),所以我们需要自己更新高度并且修正y值 - if (CGRectGetHeight(titleView.bounds) != titleViewSize.height) { - CGFloat titleViewMinY = flat(CGRectGetMinY(titleView.frame) - ((titleViewSize.height - CGRectGetHeight(titleView.bounds)) / 2.0));// 系统对titleView的y值布局是flat,注意,不能改,改了要测试 - titleView.frame = CGRectMake(CGRectGetMinX(titleView.frame), titleViewMinY, MIN(titleViewMaximumWidth, titleViewSize.width), titleViewSize.height); - } - - // iOS 11 之后(iOS 11 Beta 5 测试过) titleView 的布局发生了一些变化,如果不主动设置宽度,titleView 里的内容就可能无法完整展示 - if (@available(iOS 11, *)) { - if (CGRectGetWidth(titleView.bounds) != titleViewSize.width) { - titleView.frame = CGRectSetWidth(titleView.frame, titleViewSize.width); - } - } - } else { - titleView = nil; - } - } - - // call super - void (*originSelectorIMP)(id, SEL); - originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD); - }; - }); - }); -} - -@end +#import "UINavigationItem+QMUI.h" +#import "QMUIAppearance.h" @interface QMUINavigationTitleView () +@property(nonatomic, strong, readonly) UIView *contentView; @property(nonatomic, assign) CGSize titleLabelSize; @property(nonatomic, assign) CGSize subtitleLabelSize; @property(nonatomic, strong) UIImageView *accessoryTypeView; @@ -93,17 +48,24 @@ - (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style { - (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style frame:(CGRect)frame { if (self = [super initWithFrame:frame]) { + self.qmui_useAsNavigationTitleView = YES; + self.qmui_outsideEdge = UIEdgeInsetsMake(-10, 0, -10, 0); [self addTarget:self action:@selector(handleTouchTitleViewEvent) forControlEvents:UIControlEventTouchUpInside]; + _contentView = [[UIView alloc] init]; + [self addSubview:self.contentView]; + _titleLabel = [[UILabel alloc] init]; self.titleLabel.textAlignment = NSTextAlignmentCenter; self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; - [self addSubview:self.titleLabel]; + self.titleLabel.accessibilityTraits |= UIAccessibilityTraitHeader; + [self.contentView addSubview:self.titleLabel]; _subtitleLabel = [[UILabel alloc] init]; self.subtitleLabel.textAlignment = NSTextAlignmentCenter; self.subtitleLabel.lineBreakMode = NSLineBreakByTruncatingTail; - [self addSubview:self.subtitleLabel]; + self.subtitleLabel.accessibilityTraits |= UIAccessibilityTraitHeader; + [self.contentView addSubview:self.subtitleLabel]; self.userInteractionEnabled = NO; self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter; @@ -115,17 +77,13 @@ - (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style frame:(CGRect) self.needsLoadingPlaceholderSpace = YES; self.accessoryType = QMUINavigationTitleViewAccessoryTypeNone; - QMUINavigationTitleView *appearance = [QMUINavigationTitleView appearance]; - self.maximumWidth = appearance.maximumWidth; - self.loadingViewSize = appearance.loadingViewSize; - self.loadingViewMarginRight = appearance.loadingViewMarginRight; - self.horizontalTitleFont = appearance.horizontalTitleFont ?: [UINavigationBar appearance].titleTextAttributes[NSFontAttributeName]; - self.horizontalSubtitleFont = appearance.horizontalSubtitleFont ?: self.horizontalTitleFont; - self.verticalTitleFont = appearance.verticalTitleFont; - self.verticalSubtitleFont = appearance.verticalSubtitleFont; - self.accessoryViewOffset = appearance.accessoryViewOffset; - self.subAccessoryViewOffset = appearance.subAccessoryViewOffset; - self.tintColor = QMUICMIActivated ? NavBarTitleColor : [UINavigationBar appearance].titleTextAttributes[NSForegroundColorAttributeName]; + [self qmui_applyAppearance]; + self.horizontalTitleFont = QMUINavigationTitleView.appearance.horizontalTitleFont ?: UINavigationBar.qmui_appearanceConfigured.titleTextAttributes[NSFontAttributeName]; + self.horizontalSubtitleFont = QMUINavigationTitleView.appearance.horizontalSubtitleFont ?: self.horizontalTitleFont; + + self.adjustsSubviewsTintColorAutomatically = QMUINavigationTitleView.appearance.adjustsSubviewsTintColorAutomatically; + self.adjustsSubviewsWhenHighlighted = QMUINavigationTitleView.appearance.adjustsSubviewsWhenHighlighted; + self.tintColor = QMUICMIActivated ? NavBarTitleColor : UINavigationBar.qmui_appearanceConfigured.titleTextAttributes[NSForegroundColorAttributeName]; } return self; } @@ -144,6 +102,13 @@ - (void)refreshLayout { [self setNeedsLayout]; } +- (void)setNeedsLayout { + [self updateTitleLabelSize]; + [self updateSubtitleLabelSize]; + [self updateSubAccessoryViewHidden]; + [super setNeedsLayout]; +} + // 找到 titleView 所在的 navigationBar(iOS 11 及以后,titleView.superview.superview == navigationBar,iOS 10 及以前,titleView.superview == navigationBar) - (UINavigationBar *)navigationBarSuperviewForSubview:(UIView *)subview { if (!subview.superview) { @@ -176,14 +141,17 @@ - (void)updateSubtitleLabelSize { } - (CGSize)loadingViewSpacingSize { - if (self.needsLoadingView) { - return CGSizeMake(self.loadingViewSize.width + self.loadingViewMarginRight, self.loadingViewSize.height); + if (self.needsLoadingView && (self.needsLoadingPlaceholderSpace || !self.loadingViewHidden)) { + // 意味着希望保持 title 绝对居中,所以不管 loading 是否显示,都固定留空位给 loading + CGSize size = CGSizeMake(self.loadingViewSize.width + self.loadingViewMarginRight, self.loadingViewSize.height); + return size; } return CGSizeZero; } - (CGSize)loadingViewSpacingSizeIfNeedsPlaceholder { - return CGSizeMake([self loadingViewSpacingSize].width * (self.needsLoadingPlaceholderSpace ? 2 : 1), [self loadingViewSpacingSize].height); + CGSize size = CGSizeMake([self loadingViewSpacingSize].width * (self.needsLoadingPlaceholderSpace ? 2 : 1), [self loadingViewSpacingSize].height); + return size; } - (CGSize)accessorySpacingSize { @@ -197,7 +165,7 @@ - (CGSize)accessorySpacingSize { - (CGSize)subAccessorySpacingSize { if (self.subAccessoryView) { UIView *view = self.subAccessoryView; - return CGSizeMake(CGRectGetWidth(view.bounds) + self.subAccessoryViewOffset.x, CGRectGetHeight(view.bounds)); + return CGSizeMake(CGRectGetWidth(view.frame) + self.subAccessoryViewOffset.x, CGRectGetHeight(view.frame)); } return CGSizeZero; } @@ -218,23 +186,30 @@ - (UIEdgeInsets)subtitleEdgeInsetsIfShowingSubtitleLabel { return CGSizeIsEmpty(self.subtitleLabelSize) ? UIEdgeInsetsZero : self.subtitleEdgeInsets; } +- (CGFloat)firstLineWidthInVerticalStyle { + CGFloat firstLineWidth = self.titleLabelSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsetsIfShowingTitleLabel); + firstLineWidth += [self loadingViewSpacingSizeIfNeedsPlaceholder].width; + firstLineWidth += [self accessorySpacingSizeIfNeedesPlaceholder].width; + return firstLineWidth; +} + +- (CGFloat)secondLineWidthInVerticalStyle { + CGFloat secondLineWidth = self.subtitleLabelSize.width + UIEdgeInsetsGetHorizontalValue(self.subtitleEdgeInsetsIfShowingSubtitleLabel); + if (self.subtitleLabelSize.width > 0 && self.subAccessoryView && !self.subAccessoryView.hidden) { + secondLineWidth += [self subAccessorySpacingSizeIfNeedesPlaceholder].width; + } + return secondLineWidth; +} + - (CGSize)contentSize { if (self.style == QMUINavigationTitleViewStyleSubTitleVertical) { CGSize size = CGSizeZero; - // 垂直排列的情况下,loading和accessory与titleLabel同一行 - CGFloat firstLineWidth = self.titleLabelSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsetsIfShowingTitleLabel); - firstLineWidth += [self loadingViewSpacingSizeIfNeedsPlaceholder].width; - firstLineWidth += [self accessorySpacingSizeIfNeedesPlaceholder].width; - - CGFloat secondLineWidth = self.subtitleLabelSize.width + UIEdgeInsetsGetHorizontalValue(self.subtitleEdgeInsetsIfShowingSubtitleLabel); - if (self.subtitleLabelSize.width > 0 && self.subAccessoryView && !self.subAccessoryView.hidden) { - secondLineWidth += [self subAccessorySpacingSizeIfNeedesPlaceholder].width; - } - + CGFloat firstLineWidth = [self firstLineWidthInVerticalStyle];// 垂直排列的情况下,loading和accessory与titleLabel同一行 + CGFloat secondLineWidth = [self secondLineWidthInVerticalStyle]; size.width = MAX(firstLineWidth, secondLineWidth); - size.height = self.titleLabelSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsetsIfShowingTitleLabel) + self.subtitleLabelSize.height + UIEdgeInsetsGetVerticalValue(self.subtitleEdgeInsetsIfShowingSubtitleLabel); + size.height = self.titleLabelSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsetsIfShowingTitleLabel) + self.subtitleLabelSize.height + UIEdgeInsetsGetVerticalValue(self.subtitleEdgeInsetsIfShowingSubtitleLabel);// 虽然 accessoryView、loadingView 的高度都可能超过文字本身高度,但为了方便,这里就只考虑文字高度,其他 subview 高度均不考虑,布局时都相对于文字垂直居中(可能会溢出 titleView 的上下边缘) return CGSizeFlatted(size); } else { CGSize size = CGSizeZero; @@ -249,7 +224,9 @@ - (CGSize)contentSize { - (CGSize)sizeThatFits:(CGSize)size { CGSize resultSize = [self contentSize]; + resultSize.width += UIEdgeInsetsGetHorizontalValue(self.padding); resultSize.width = MIN(resultSize.width, self.maximumWidth); + resultSize.height += UIEdgeInsetsGetVerticalValue(self.padding); return resultSize; } @@ -260,29 +237,22 @@ - (void)layoutSubviews { [super layoutSubviews]; + self.contentView.frame = CGRectInsetEdges(self.bounds, self.padding); + BOOL alignLeft = self.contentHorizontalAlignment == UIControlContentHorizontalAlignmentLeft; BOOL alignRight = self.contentHorizontalAlignment == UIControlContentHorizontalAlignmentRight; // 通过sizeThatFit计算出来的size,如果大于可使用的最大宽度,则会被系统改为最大限制的最大宽度 - CGSize maxSize = self.bounds.size; + CGSize maxSize = self.contentView.bounds.size; // 实际内容的size,小于等于maxSize CGSize contentSize = [self contentSize]; contentSize.width = MIN(maxSize.width, contentSize.width); contentSize.height = MIN(maxSize.height, contentSize.height); - // 计算左右两边的偏移值 - CGFloat offsetLeft = 0; - CGFloat offsetRight = 0; - if (alignLeft) { - offsetLeft = 0; - offsetRight = maxSize.width - contentSize.width; - } else if (alignRight) { - offsetLeft = maxSize.width - contentSize.width; - offsetRight = 0; - } else { - offsetLeft = offsetRight = floorInPixel((maxSize.width - contentSize.width) / 2.0); - } + // 整个 titleView 居中,但内部的 subviews 可以在内容区域根据 contentHorizontalAlignment 的值做不一样的对齐布局 + CGFloat contentOffsetLeft = floorInPixel((maxSize.width - contentSize.width) / 2.0); + CGFloat contentOffsetRight = contentOffsetLeft; // 计算loading占的单边宽度 CGFloat loadingViewSpace = [self loadingViewSpacingSize].width; @@ -299,59 +269,69 @@ - (void)layoutSubviews { UIEdgeInsets titleEdgeInsets = self.titleEdgeInsetsIfShowingTitleLabel; UIEdgeInsets subtitleEdgeInsets = self.subtitleEdgeInsetsIfShowingSubtitleLabel; - CGFloat minX = offsetLeft + (self.needsAccessoryPlaceholderSpace ? accessoryViewSpace : 0); - CGFloat maxX = maxSize.width - offsetRight - (self.needsLoadingPlaceholderSpace ? loadingViewSpace : 0); - if (self.style == QMUINavigationTitleViewStyleSubTitleVertical) { - + CGFloat firstLineWidth = [self firstLineWidthInVerticalStyle];// 这里得到的是实际内容宽度,可能会超出 contentSize.width,所以下方会用 MIN/MAX 做保护 + CGFloat firstLineMinX = 0; + CGFloat firstLineMaxX = 0; + if (alignLeft) { + firstLineMinX = contentOffsetLeft; + } else if (alignRight) { + firstLineMinX = MAX(contentOffsetLeft, contentOffsetLeft + contentSize.width - firstLineWidth); + } else { + firstLineMinX = contentOffsetLeft + MAX(0, CGFloatGetCenter(contentSize.width, firstLineWidth)); + } + firstLineMaxX = firstLineMinX + MIN(firstLineWidth, contentSize.width) - (self.needsLoadingPlaceholderSpace ? [self loadingViewSpacingSize].width : 0); + firstLineMinX += self.needsAccessoryPlaceholderSpace ? accessoryViewSpace : 0; if (self.loadingView) { - self.loadingView.frame = CGRectSetXY(self.loadingView.frame, minX, CGFloatGetCenter(self.titleLabelSize.height, self.loadingViewSize.height) + titleEdgeInsets.top); - minX = CGRectGetMaxX(self.loadingView.frame) + self.loadingViewMarginRight; + if (self.needsLoadingPlaceholderSpace || !self.loadingView.hidden) { + self.loadingView.frame = CGRectSetXY(self.loadingView.frame, firstLineMinX, CGFloatGetCenter(self.titleLabelSize.height, self.loadingViewSize.height) + titleEdgeInsets.top); + firstLineMinX = CGRectGetMaxX(self.loadingView.frame) + self.loadingViewMarginRight; + } } if (accessoryView) { - accessoryView.frame = CGRectSetXY(accessoryView.frame, maxX - CGRectGetWidth(accessoryView.bounds), CGFloatGetCenter(self.titleLabelSize.height, CGRectGetHeight(accessoryView.bounds)) + titleEdgeInsets.top + self.accessoryViewOffset.y); - maxX = CGRectGetMinX(accessoryView.frame) - self.accessoryViewOffset.x; + accessoryView.frame = CGRectSetXY(accessoryView.frame, firstLineMaxX - CGRectGetWidth(accessoryView.frame), CGFloatGetCenter(self.titleLabelSize.height, CGRectGetHeight(accessoryView.frame)) + titleEdgeInsets.top + self.accessoryViewOffset.y); + firstLineMaxX = CGRectGetMinX(accessoryView.frame) - self.accessoryViewOffset.x; } if (isTitleLabelShowing) { - minX += titleEdgeInsets.left; - maxX -= titleEdgeInsets.right; - self.titleLabel.frame = CGRectFlatMake(minX, titleEdgeInsets.top, maxX - minX, self.titleLabelSize.height); + firstLineMinX += titleEdgeInsets.left; + firstLineMaxX -= titleEdgeInsets.right; + self.titleLabel.frame = CGRectFlatMake(firstLineMinX, titleEdgeInsets.top, firstLineMaxX - firstLineMinX, self.titleLabelSize.height); } else { self.titleLabel.frame = CGRectZero; } + if (isSubtitleLabelShowing) { - [self.subtitleLabel sizeToFit]; - CGFloat subtitleMinX = subtitleEdgeInsets.left; - CGFloat subtitleWidth = maxSize.width - UIEdgeInsetsGetHorizontalValue(subtitleEdgeInsets); - - if (isSubAccessoryViewShowing) { - if (self.needsSubAccessoryPlaceholderSpace) { - // 副标题居中,accessoryView 在右边 - CGFloat subtitleMaxWidth = subtitleWidth - [self subAccessorySpacingSizeIfNeedesPlaceholder].width; - subtitleWidth = MIN(CGRectGetWidth(self.subtitleLabel.bounds), subtitleMaxWidth); - subtitleMinX = (CGRectGetWidth(self.bounds) - subtitleWidth) / 2.0; - } else { - // 整体居中 - CGFloat subtitleMaxWidth = subtitleWidth - [self subAccessorySpacingSize].width; - subtitleWidth = MIN(CGRectGetWidth(self.subtitleLabel.bounds), subtitleMaxWidth); - subtitleMinX = (CGRectGetWidth(self.bounds) - subtitleWidth - [self subAccessorySpacingSize].width) / 2.0; - } + CGFloat secondLineWidth = [self secondLineWidthInVerticalStyle]; + CGFloat secondLineMinX = 0; + CGFloat secondLineMaxX = 0; + CGFloat secondLineMinY = subtitleEdgeInsets.top + (isTitleLabelShowing ? CGRectGetMaxY(self.titleLabel.frame) + titleEdgeInsets.bottom : 0); + if (alignLeft) { + secondLineMinX = contentOffsetLeft; + } else if (alignRight) { + secondLineMinX = MAX(contentOffsetLeft, contentOffsetLeft + contentSize.width - secondLineWidth); + } else { + secondLineMinX = contentOffsetLeft + MAX(0, CGFloatGetCenter(contentSize.width, secondLineWidth)); } - - self.subtitleLabel.frame = CGRectFlatMake(subtitleMinX, (isTitleLabelShowing ? CGRectGetMaxY(self.titleLabel.frame) + titleEdgeInsets.bottom : 0) + subtitleEdgeInsets.top, subtitleWidth, self.subtitleLabelSize.height); - + secondLineMaxX = secondLineMinX + MIN(secondLineWidth, contentSize.width); + secondLineMinX += self.needsSubAccessoryPlaceholderSpace ? [self subAccessorySpacingSize].width : 0; if (isSubAccessoryViewShowing) { - self.subAccessoryView.frame = CGRectSetXY(self.subAccessoryView.frame, CGRectGetMaxX(self.subtitleLabel.frame) + self.subAccessoryViewOffset.x, CGRectGetMinYVerticallyCenter(self.subtitleLabel.frame, self.subAccessoryView.frame) + self.subAccessoryViewOffset.y); + self.subAccessoryView.frame = CGRectSetXY(self.subAccessoryView.frame, secondLineMaxX - CGRectGetWidth(self.subAccessoryView.frame), secondLineMinY + CGFloatGetCenter(self.subtitleLabelSize.height, CGRectGetHeight(self.subAccessoryView.frame)) + self.subAccessoryViewOffset.y); + secondLineMaxX = CGRectGetMinX(self.subAccessoryView.frame) - self.subAccessoryViewOffset.x; } + self.subtitleLabel.frame = CGRectFlatMake(secondLineMinX, secondLineMinY, secondLineMaxX - secondLineMinX, self.subtitleLabelSize.height); } else { self.subtitleLabel.frame = CGRectZero; } } else { + CGFloat minX = contentOffsetLeft + (self.needsAccessoryPlaceholderSpace ? accessoryViewSpace : 0); + CGFloat maxX = maxSize.width - contentOffsetRight - (self.needsLoadingPlaceholderSpace ? loadingViewSpace : 0); if (self.loadingView) { - self.loadingView.frame = CGRectSetXY(self.loadingView.frame, minX, CGFloatGetCenter(maxSize.height, self.loadingViewSize.height)); - minX = CGRectGetMaxX(self.loadingView.frame) + self.loadingViewMarginRight; + if (self.needsLoadingPlaceholderSpace || !self.loadingView.hidden) { + self.loadingView.frame = CGRectSetXY(self.loadingView.frame, minX, CGFloatGetCenter(maxSize.height, self.loadingViewSize.height)); + minX = CGRectGetMaxX(self.loadingView.frame) + self.loadingViewMarginRight; + } } if (accessoryView) { accessoryView.frame = CGRectSetXY(accessoryView.frame, maxX - CGRectGetWidth(accessoryView.bounds), CGFloatGetCenter(maxSize.height, CGRectGetHeight(accessoryView.bounds)) + self.accessoryViewOffset.y); @@ -362,7 +342,7 @@ - (void)layoutSubviews { // 如果当前的 contentSize 就是以这个 label 的最大占位计算出来的,那么就不应该先计算 center 再计算偏移 BOOL shouldSubtitleLabelCenterVertically = self.subtitleLabelSize.height + UIEdgeInsetsGetVerticalValue(subtitleEdgeInsets) < contentSize.height; CGFloat subtitleMinY = shouldSubtitleLabelCenterVertically ? CGFloatGetCenter(maxSize.height, self.subtitleLabelSize.height) + subtitleEdgeInsets.top - subtitleEdgeInsets.bottom : subtitleEdgeInsets.top; - self.subtitleLabel.frame = CGRectFlatMake(maxX - self.subtitleLabelSize.width, subtitleMinY, self.subtitleLabelSize.width, self.subtitleLabelSize.height); + self.subtitleLabel.frame = CGRectFlatMake(MAX(minX + subtitleEdgeInsets.left, maxX - self.subtitleLabelSize.width), subtitleMinY, MIN(self.subtitleLabelSize.width, maxX - minX - subtitleEdgeInsets.left), self.subtitleLabelSize.height); maxX = CGRectGetMinX(self.subtitleLabel.frame) - subtitleEdgeInsets.left; } else { self.subtitleLabel.frame = CGRectZero; @@ -402,6 +382,11 @@ - (void)setMaximumWidth:(CGFloat)maximumWidth { [self refreshLayout]; } +- (void)setPadding:(UIEdgeInsets)padding { + _padding = padding; + [self refreshLayout]; +} + - (void)setContentHorizontalAlignment:(UIControlContentHorizontalAlignment)contentHorizontalAlignment { [super setContentHorizontalAlignment:contentHorizontalAlignment]; [self refreshLayout]; @@ -441,7 +426,6 @@ - (void)setHorizontalTitleFont:(UIFont *)horizontalTitleFont { _horizontalTitleFont = horizontalTitleFont; if (self.style == QMUINavigationTitleViewStyleDefault) { self.titleLabel.font = horizontalTitleFont; - [self updateTitleLabelSize]; [self refreshLayout]; } } @@ -450,7 +434,6 @@ - (void)setHorizontalSubtitleFont:(UIFont *)horizontalSubtitleFont { _horizontalSubtitleFont = horizontalSubtitleFont; if (self.style == QMUINavigationTitleViewStyleDefault) { self.subtitleLabel.font = horizontalSubtitleFont; - [self updateSubtitleLabelSize]; [self refreshLayout]; } } @@ -459,7 +442,6 @@ - (void)setVerticalTitleFont:(UIFont *)verticalTitleFont { _verticalTitleFont = verticalTitleFont; if (self.style == QMUINavigationTitleViewStyleSubTitleVertical) { self.titleLabel.font = verticalTitleFont; - [self updateTitleLabelSize]; [self refreshLayout]; } } @@ -468,7 +450,6 @@ - (void)setVerticalSubtitleFont:(UIFont *)verticalSubtitleFont { _verticalSubtitleFont = verticalSubtitleFont; if (self.style == QMUINavigationTitleViewStyleSubTitleVertical) { self.subtitleLabel.font = verticalSubtitleFont; - [self updateSubtitleLabelSize]; [self refreshLayout]; } } @@ -486,15 +467,12 @@ - (void)setSubtitleEdgeInsets:(UIEdgeInsets)subtitleEdgeInsets { - (void)setTitle:(NSString *)title { _title = title; self.titleLabel.text = title; - [self updateTitleLabelSize]; [self refreshLayout]; } - (void)setSubtitle:(NSString *)subtitle { _subtitle = subtitle; self.subtitleLabel.text = subtitle; - [self updateSubtitleLabelSize]; - [self updateSubAccessoryViewHidden]; [self refreshLayout]; } @@ -529,7 +507,7 @@ - (void)setAccessoryType:(QMUINavigationTitleViewAccessoryType)accessoryType { // 经过上面的 setImage 和 sizeToFit 之后再 addSubview,因为 addSubview 会触发系统来询问你的 sizeThatFits: if (self.accessoryTypeView.superview != self) { - [self addSubview:self.accessoryTypeView]; + [self.contentView addSubview:self.accessoryTypeView]; } [self refreshLayout]; @@ -544,7 +522,7 @@ - (void)setAccessoryView:(UIView *)accessoryView { _accessoryView = accessoryView; self.accessoryType = QMUINavigationTitleViewAccessoryTypeNone; [self.accessoryView sizeToFit]; - [self addSubview:self.accessoryView]; + [self.contentView addSubview:self.accessoryView]; } [self refreshLayout]; } @@ -557,15 +535,14 @@ - (void)setSubAccessoryView:(UIView *)subAccessoryView { if (subAccessoryView) { _subAccessoryView = subAccessoryView; [self.subAccessoryView sizeToFit]; - [self addSubview:self.subAccessoryView]; + [self.contentView addSubview:self.subAccessoryView]; } - [self updateSubAccessoryViewHidden]; [self refreshLayout]; } - (void)updateSubAccessoryViewHidden { - if (self.subAccessoryView && self.subtitle.length && self.style == QMUINavigationTitleViewStyleSubTitleVertical) { + if (self.subAccessoryView && self.subtitleLabel.text.length && self.style == QMUINavigationTitleViewStyleSubTitleVertical) { self.subAccessoryView.hidden = NO; } else { self.subAccessoryView.hidden = YES; @@ -576,10 +553,11 @@ - (void)setNeedsLoadingView:(BOOL)needsLoadingView { _needsLoadingView = needsLoadingView; if (needsLoadingView) { if (!self.loadingView) { - _loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:NavBarActivityIndicatorViewStyle size:self.loadingViewSize]; + _loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:NavBarActivityIndicatorViewStyle]; + self.loadingView.qmui_size = self.loadingViewSize; self.loadingView.color = self.tintColor; [self.loadingView stopAnimating]; - [self addSubview:self.loadingView]; + [self.contentView addSubview:self.loadingView]; } } else { if (self.loadingView) { @@ -599,6 +577,14 @@ - (void)setLoadingViewHidden:(BOOL)loadingViewHidden { [self refreshLayout]; } +- (void)setLoadingViewSize:(CGSize)loadingViewSize { + _loadingViewSize = loadingViewSize; + if (self.loadingView) { + self.loadingView.qmui_size = loadingViewSize; + [self refreshLayout]; + } +} + - (void)setActive:(BOOL)active { _active = active; if ([self.delegate respondsToSelector:@selector(didChangedActive:forTitleView:)]) { @@ -625,36 +611,41 @@ - (void)setStyle:(QMUINavigationTitleViewStyle)style { _style = style; if (style == QMUINavigationTitleViewStyleSubTitleVertical) { self.titleLabel.font = self.verticalTitleFont; - [self updateTitleLabelSize]; - self.subtitleLabel.font = self.verticalSubtitleFont; - [self updateSubtitleLabelSize]; } else { self.titleLabel.font = self.horizontalTitleFont; - [self updateTitleLabelSize]; - self.subtitleLabel.font = self.horizontalSubtitleFont; - [self updateSubtitleLabelSize]; } - [self updateSubAccessoryViewHidden]; [self refreshLayout]; } - (void)tintColorDidChange { [super tintColorDidChange]; - UIColor *color = self.tintColor; - self.titleLabel.textColor = color; - self.subtitleLabel.textColor = color; - self.loadingView.color = color; + if (self.adjustsSubviewsTintColorAutomatically) { + UIColor *color = self.tintColor; + self.titleLabel.textColor = color; + self.subtitleLabel.textColor = color; + self.loadingView.color = color; + } } #pragma mark - Events - (void)setHighlighted:(BOOL)highlighted { [super setHighlighted:highlighted]; - self.alpha = highlighted ? UIControlHighlightedAlpha : 1; + if (self.adjustsSubviewsWhenHighlighted) { + self.alpha = highlighted ? UIControlHighlightedAlpha : 1; + } +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + UIView *result = [super hitTest:point withEvent:event]; + if (result == self.contentView) { + return self; + } + return result; } - (void)handleTouchTitleViewEvent { @@ -666,6 +657,60 @@ - (void)handleTouchTitleViewEvent { [self refreshLayout]; } ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // 让 -[UIViewController setTitle:] 可以自动刷新 QMUINavigationTitle + OverrideImplementation([UIViewController class], @selector(setTitle:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIViewController *selfObject, NSString *title) { + + // call super + void (*originSelectorIMP)(id, SEL, NSString *); + originSelectorIMP = (void (*)(id, SEL, NSString *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, title); + + if ([selfObject.navigationItem.titleView isKindOfClass:QMUINavigationTitleView.class]) { + ((QMUINavigationTitleView *)selfObject.navigationItem.titleView).title = title; + } + }; + }); + + // 让 -[UINavigationItem setTitle:] 可以自动刷新 QMUINavigationTitleView + OverrideImplementation([UINavigationItem class], @selector(setTitle:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationItem *selfObject, NSString *title) { + + // call super + void (*originSelectorIMP)(id, SEL, NSString *); + originSelectorIMP = (void (*)(id, SEL, NSString *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, title); + + if ([selfObject.titleView isKindOfClass:QMUINavigationTitleView.class]) { + ((QMUINavigationTitleView *)selfObject.titleView).title = title; + } + }; + }); + + // 在先设置了 title 再设置 titleView 时,保证 titleView 的 title 能正确。 + OverrideImplementation([UINavigationItem class], @selector(setTitleView:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationItem *selfObject, QMUINavigationTitleView *titleView) { + + // call super + void (*originSelectorIMP)(id, SEL, UIView *); + originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, titleView); + + if ([titleView isKindOfClass:QMUINavigationTitleView.class]) { + if (titleView.title.length <= 0) { + NSString *title = selfObject.qmui_viewController.title ?: selfObject.title; + titleView.title = title; + } + } + }; + }); + }); +} + @end @interface QMUINavigationTitleView (UIAppearance) @@ -683,6 +728,8 @@ + (void)initialize { + (void)setDefaultAppearance { QMUINavigationTitleView *appearance = [QMUINavigationTitleView appearance]; + appearance.adjustsSubviewsTintColorAutomatically = YES; + appearance.adjustsSubviewsWhenHighlighted = YES; appearance.maximumWidth = CGFLOAT_MAX; appearance.loadingViewSize = CGSizeMake(18, 18); appearance.loadingViewMarginRight = 3; @@ -697,3 +744,115 @@ + (void)setDefaultAppearance { } @end + +#pragma mark - LargeTitle 兼容 + +@implementation QMUINavigationTitleView (LargeTitleCompatibility) + +- (void)setAlpha:(BOOL)alpha animated:(BOOL)animated { + // 在 push 和 pop 过渡期间系统会对自定义的 titleView 的 alpha 进行调整,了避免和系统的设置冲突(比如设置 alpha 为 0 又被系统还原为 1)这里通过设置 contentView 的 alpha 来控制整个 QMUINavigationTitleView 显示与隐藏。 + [UIView qmui_animateWithAnimated:animated duration:0.25f animations:^{ + self.contentView.alpha = alpha; + }]; +} + +@end + +@implementation UINavigationBar (LargeTitleCompatibility) + +- (UIView *)qmui_largeTitleView { + for (UIView *subview in self.subviews) { + if ([NSStringFromClass(subview.class) hasSuffix:@"LargeTitleView"]) { + return subview; + } + } + return nil; +} + +@end + + +@implementation UINavigationController(LargeTitleCompatibility) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // -[UINavigationController _updateTopViewFramesToMatchScrollOffsetInViewController:contentScrollView:topLayoutType:] + OverrideImplementation([UINavigationController class], sel_registerName("_updateTopViewFramesToMatchScrollOffsetInViewController:contentScrollView:topLayoutType:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationController *selfObject, UIViewController *viewController, UIScrollView *scrollView, NSUInteger topLayoutType) { + + // call super + void (*originSelectorIMP)(id, SEL, UIViewController *, UIScrollView *, NSUInteger); + originSelectorIMP = (void (*)(id, SEL, UIViewController *, UIScrollView *, NSUInteger))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, viewController, scrollView, topLayoutType); + + [selfObject qmui_updateTitleViewToMatchScrollOffsetInViewController:viewController contentScrollView:scrollView topLayoutType:topLayoutType]; + }; + }); + }); +} + +- (void)qmui_updateTitleViewToMatchScrollOffsetInViewController:(UIViewController *)viewController contentScrollView:(UIScrollView *)contentScrollView topLayoutType:(NSInteger)topLayoutType { + UIView *titleView = viewController.navigationItem.titleView; + if (!titleView || ![titleView isKindOfClass:[QMUINavigationTitleView class]]) { + return; + } + + if (viewController.navigationController != self) return; + + QMUINavigationTitleView *navigationTitleView = (QMUINavigationTitleView *)titleView; + UIView *largeTitleView = self.navigationBar.qmui_largeTitleView; + BOOL largeTitleLabelVisable = self.navigationBar.prefersLargeTitles && viewController.qmui_prefersLargeTitleDisplayed && largeTitleView.alpha != 0; + BOOL titleViewAlpha = largeTitleLabelVisable ? 0 : 1; + BOOL animated = contentScrollView.layer.presentationLayer && !CGRectEqualToRect(contentScrollView.layer.presentationLayer.bounds, contentScrollView.layer.bounds); + [navigationTitleView setAlpha:titleViewAlpha animated:animated]; +} + + +@end + +@implementation UIView (QMUINavigationTitleView) + +static char kAssociatedObjectKey_useAsNavigationTitleView; +- (void)setQmui_useAsNavigationTitleView:(BOOL)useAsNavigationTitleView { + objc_setAssociatedObject(self, &kAssociatedObjectKey_useAsNavigationTitleView, @(useAsNavigationTitleView), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (useAsNavigationTitleView) { + [QMUIHelper executeBlock:^{ + // 修复系统使用自定义 titleView 时的布局问题 + OverrideImplementation([UINavigationBar class], @selector(layoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject) { + + UIView *titleView = selfObject.topItem.titleView; + + if (titleView.qmui_useAsNavigationTitleView) { + CGFloat titleViewMaximumWidth = CGRectGetWidth(titleView.bounds);// 初始状态下titleView会被设置为UINavigationBar允许的最大宽度 + CGSize titleViewSize = [titleView sizeThatFits:CGSizeMake(titleViewMaximumWidth, CGFLOAT_MAX)]; + titleViewSize.height = ceil(titleViewSize.height);// titleView的高度如果非pt整数,会导致计算出来的y值时多时少,所以干脆做一下pt取整,这个策略不要改,改了要重新测试push过程中titleView是否会跳动 + + // 当在UINavigationBar里使用自定义的titleView时,就算titleView的sizeThatFits:返回正确的高度,navigationBar也不会帮你设置高度(但会帮你设置宽度),所以我们需要自己更新高度并且修正y值 + if (CGRectGetHeight(titleView.bounds) != titleViewSize.height) { + CGFloat titleViewMinY = flat(CGRectGetMinY(titleView.frame) - ((titleViewSize.height - CGRectGetHeight(titleView.bounds)) / 2.0));// 系统对titleView的y值布局是flat,注意,不能改,改了要测试 + titleView.frame = CGRectMake(CGRectGetMinX(titleView.frame), titleViewMinY, MIN(titleViewMaximumWidth, titleViewSize.width), titleViewSize.height); + } + + // iOS 11 之后(iOS 11 Beta 5 测试过) titleView 的布局发生了一些变化,如果不主动设置宽度,titleView 里的内容就可能无法完整展示 + if (CGRectGetWidth(titleView.bounds) != titleViewSize.width) { + titleView.frame = CGRectSetWidth(titleView.frame, titleViewSize.width); + } + } + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + }; + }); + } oncePerIdentifier:@"UIView (QMUINavigationTitleView)"]; + } +} + +- (BOOL)qmui_useAsNavigationTitleView { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_useAsNavigationTitleView)) boolValue]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.h b/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.h index 6e5af72a..09f30fdf 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIOrderedDictionary.h @@ -16,13 +16,18 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + +/** + 一个简单实现的有序的 key-value 容器,通过 initWithKeysAndObjects: 初始化后,用下标访问即可,如 dict[0] 或 dict[key] + */ @interface QMUIOrderedDictionary<__covariant KeyType, __covariant ObjectType> : NSObject - (instancetype)initWithKeysAndObjects:(id)firstKey,...; @property(readonly) NSUInteger count; @property(nonatomic, copy, readonly) NSArray *allKeys; - +@property(nonatomic, copy, readonly) NSArray *allValues; - (void)setObject:(ObjectType)object forKey:(KeyType)key; - (void)addObject:(ObjectType)object forKey:(KeyType)key; - (void)addObjects:(NSArray *)objects forKeys:(NSArray *)keys; @@ -30,7 +35,13 @@ - (void)insertObjects:(NSArray *)objects forKeys:(NSArray *)keys atIndex:(NSInteger)index; - (void)removeObject:(ObjectType)object forKey:(KeyType)key; - (void)removeObject:(ObjectType)object atIndex:(NSInteger)index; -- (ObjectType)objectForKey:(KeyType)key; +- (nullable ObjectType)objectForKey:(KeyType)key; - (ObjectType)objectAtIndex:(NSInteger)index; +// 支持下标的方式访问,需要声明以下两个方法 +- (nullable ObjectType)objectForKeyedSubscript:(KeyType)key; +- (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx; + @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.m b/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.m index 1ad98aaa..c17c093c 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIOrderedDictionary.m @@ -69,6 +69,10 @@ - (NSArray *)allKeys { return self.mutableAllKeys.copy; } +- (NSArray *)allValues { + return self.mutableAllValues.copy; +} + - (void)setObject:(id)object forKey:(id)key { if ([self.mutableAllKeys containsObject:key]) { NSInteger index = [self.mutableAllKeys indexOfObject:key]; @@ -132,10 +136,18 @@ - (id)objectForKey:(id)key { return [self.mutableDictionary objectForKey:key]; } +- (id)objectForKeyedSubscript:(id)key { + return [self objectForKey:key]; +} + - (id)objectAtIndex:(NSInteger)index { return [self.mutableDictionary objectForKey:self.mutableAllKeys[index]]; } +- (id)objectAtIndexedSubscript:(NSUInteger)idx { + return [self objectAtIndex:idx]; +} + - (NSString *)description { return [NSString stringWithFormat:@"%@, %@", [super description], self.mutableDictionary]; } diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.h b/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.h index e84e0206..2ba4fae5 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIPieProgressView.h @@ -24,6 +24,12 @@ * * 通过 `UIControlEventValueChanged` 来监听进度变化 */ + +typedef NS_ENUM(NSUInteger, QMUIPieProgressViewShape) { + QMUIPieProgressViewShapeSector, // 扇形,默认 + QMUIPieProgressViewShapeRing // 环形 +}; + @interface QMUIPieProgressView : UIControl /** @@ -46,6 +52,16 @@ */ @property(nonatomic, assign) IBInspectable CGFloat borderInset; +/** + 线宽,用于环形绘制,默认为 0。 + */ +@property(nonatomic, assign) IBInspectable CGFloat lineWidth; + +/** + 绘制形状,默认是扇形。 + */ +@property(nonatomic, assign) IBInspectable QMUIPieProgressViewShape shape; + /** 修改当前的进度,会触发 UIControlEventValueChanged 事件 @@ -53,4 +69,5 @@ @param animated 是否以动画来表现 */ - (void)setProgress:(float)progress animated:(BOOL)animated; + @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.m b/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.m index dfd509f2..605bf521 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIPieProgressView.m @@ -19,16 +19,24 @@ @interface QMUIPieProgressLayer : CALayer @property(nonatomic, strong) UIColor *fillColor; +@property(nonatomic, strong) UIColor *strokeColor; @property(nonatomic, assign) float progress; @property(nonatomic, assign) CFTimeInterval progressAnimationDuration; @property(nonatomic, assign) BOOL shouldChangeProgressWithAnimation; // default is YES @property(nonatomic, assign) CGFloat borderInset; +@property(nonatomic, assign) CGFloat lineWidth; +@property(nonatomic, assign) QMUIPieProgressViewShape shape; + @end @implementation QMUIPieProgressLayer // 加dynamic才能让自定义的属性支持动画 @dynamic fillColor; +@dynamic strokeColor; @dynamic progress; +@dynamic shape; +@dynamic lineWidth; +@dynamic borderInset; - (instancetype)init { if (self = [super init]) { @@ -56,23 +64,41 @@ - (void)drawInContext:(CGContextRef)context { return; } - // 绘制扇形进度区域 CGPoint center = CGPointGetCenterWithRect(self.bounds); CGFloat radius = MIN(center.x, center.y) - self.borderWidth - self.borderInset; CGFloat startAngle = -M_PI_2; CGFloat endAngle = M_PI * 2 * self.progress + startAngle; - CGContextSetFillColorWithColor(context, self.fillColor.CGColor); - CGContextMoveToPoint(context, center.x, center.y); - CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 0); - CGContextClosePath(context); - CGContextFillPath(context); + + switch (self.shape) { + case QMUIPieProgressViewShapeSector: { + // 绘制扇形进度区域 + + CGContextSetFillColorWithColor(context, self.fillColor.CGColor); + CGContextMoveToPoint(context, center.x, center.y); + CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 0); + CGContextClosePath(context); + CGContextFillPath(context); + } + break; + + case QMUIPieProgressViewShapeRing: { + // 绘制环形进度区域 + + radius -= self.lineWidth; + CGContextSetLineWidth(context, self.lineWidth); + CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor); + CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 0); + CGContextStrokePath(context); + } + break; + } [super drawInContext:context]; } -- (void)setFrame:(CGRect)frame { - [super setFrame:frame]; - self.cornerRadius = CGRectGetHeight(frame) / 2; +- (void)layoutSublayers { + [super layoutSublayers]; + self.cornerRadius = CGRectGetHeight(self.bounds) / 2; } @end @@ -118,9 +144,8 @@ - (void)setProgress:(float)progress { - (void)setProgress:(float)progress animated:(BOOL)animated { _progress = fmax(0.0, fmin(1.0, progress)); - QMUIPieProgressLayer *layer = (QMUIPieProgressLayer *)self.layer; - layer.shouldChangeProgressWithAnimation = animated; - layer.progress = _progress; + self.progressLayer.shouldChangeProgressWithAnimation = animated; + self.progressLayer.progress = _progress; [self sendActionsForControlEvents:UIControlEventValueChanged]; } @@ -132,7 +157,7 @@ - (void)setProgressAnimationDuration:(CFTimeInterval)progressAnimationDuration { - (void)setBorderWidth:(CGFloat)borderWidth { _borderWidth = borderWidth; - self.layer.borderWidth = borderWidth; + self.progressLayer.borderWidth = borderWidth; } - (void)setBorderInset:(CGFloat)borderInset { @@ -140,12 +165,24 @@ - (void)setBorderInset:(CGFloat)borderInset { self.progressLayer.borderInset = borderInset; } +- (void)setLineWidth:(CGFloat)lineWidth { + _lineWidth = lineWidth; + self.progressLayer.lineWidth = lineWidth; +} + - (void)tintColorDidChange { [super tintColorDidChange]; self.progressLayer.fillColor = self.tintColor; + self.progressLayer.strokeColor = self.tintColor; self.progressLayer.borderColor = self.tintColor.CGColor; } +- (void)setShape:(QMUIPieProgressViewShape)shape { + _shape = shape; + self.progressLayer.shape = shape; + [self setBorderWidth:_borderWidth]; +} + - (QMUIPieProgressLayer *)progressLayer { return (QMUIPieProgressLayer *)self.layer; } diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.h index 38cddbca..2da5a690 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIPopupContainerView.h @@ -16,13 +16,23 @@ #import #import "UIControl+QMUI.h" +NS_ASSUME_NONNULL_BEGIN + typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) { QMUIPopupContainerViewLayoutDirectionAbove, - QMUIPopupContainerViewLayoutDirectionBelow + QMUIPopupContainerViewLayoutDirectionBelow, + QMUIPopupContainerViewLayoutDirectionLeft, + QMUIPopupContainerViewLayoutDirectionRight +}; + +typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutAlignment) { + QMUIPopupContainerViewLayoutAlignmentCenter, + QMUIPopupContainerViewLayoutAlignmentLeading, + QMUIPopupContainerViewLayoutAlignmentTrailing, }; /** - * 带箭头的小tips浮层,自带 imageView 和 textLabel,可展示简单的图文信息。 + * 带箭头的小tips浮层,自带 imageView 和 textLabel,可展示简单的图文信息,支持 UIViewContentModeTop/UIViewContentModeBottom/UIViewContentModeCenter 三种布局方式。 * QMUIPopupContainerView 支持以两种方式显示在界面上: * 1. 添加到某个 UIView 上(适合于 viewController 切换时浮层跟着一起切换的场景),这种场景只能手动隐藏浮层。 * 2. 在 QMUIPopupContainerView 自带的 UIWindow 里显示(适合于用完就消失的场景,不要涉及界面切换),这种场景支持点击空白地方自动隐藏浮层。 @@ -44,10 +54,13 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) { * 3. 通过重写 sizeThatFitsInContentView:,在里面返回当前 subviews 的大小。 * 4. 在 layoutSubviews: 里,所有 subviews 请相对于 contentView 布局。 */ - @interface QMUIPopupContainerView : UIControl { CAShapeLayer *_backgroundLayer; + CAShapeLayer *_borderLayer;// CAShapeLayer 的特性是有一半 stroke 会和 fill 重叠,而我们希望的是 stroke 在 fill 外面,所以只能分开两个 layer 实现 border 和 background + UIImageView *_arrowImageView; CGFloat _arrowMinX; + CGFloat _arrowMinY; + BOOL _shouldInvalidateLayout; } @property(nonatomic, assign) BOOL debug; @@ -56,14 +69,23 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) { /// 默认为 NO,也即需要手动调用代码去隐藏浮层。 @property(nonatomic, assign) BOOL automaticallyHidesWhenUserTap; -/// 所有subview都应该添加到contentView上,默认contentView.userInteractionEnabled = NO,需要事件操作时自行打开 +/// 所有 subview 都应该添加到 contentView 上,subviews 占据的大小通过 sizeThatFitsInContentView: 或者 contentViewSizeThatFitsBlock 来返回,subviews 的布局通过重写 layoutSubviews 或 qmui_layoutSubviewsBlock 来布局,注意布局时应该基于 contentView。 @property(nonatomic, strong, readonly) UIView *contentView; +/** + 与 sizeThatFitsInContentView: 等价,用于告诉组件,添加到 contentView 上的 subviews 的大小 + + @param size 浮层里除去 safetyMarginsOfSuperview、arrowSize、contentEdgeInsets 之外后,留给内容的实际大小,计算 subview 大小时均应使用这个参数来计算 + @return 自定义内容实际占据的大小 + @note 计算结果不需要操心 maximumWidth、minimumWidth,这些会由组件统一处理,你只需要在这个 block 里返回内容的实际大小即可。 + */ +@property(nonatomic, copy, nullable) CGSize (^contentViewSizeThatFitsBlock)(CGSize size); + /// 预提供的UIImageView,默认为nil,调用到的时候才初始化 -@property(nonatomic, strong, readonly) UIImageView *imageView; +@property(nonatomic, strong, readonly, nullable) UIImageView *imageView; /// 预提供的UILabel,默认为nil,调用到的时候才初始化。默认支持多行。 -@property(nonatomic, strong, readonly) UILabel *textLabel; +@property(nonatomic, strong, readonly, nullable) UILabel *textLabel; /// 圆角矩形气泡内的padding(不包括三角箭头),默认是(8, 8, 8, 8) @property(nonatomic, assign) UIEdgeInsets contentEdgeInsets UI_APPEARANCE_SELECTOR; @@ -77,13 +99,18 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) { /// 三角箭头的大小,默认为 CGSizeMake(18, 9) @property(nonatomic, assign) CGSize arrowSize UI_APPEARANCE_SELECTOR; +/// 三角箭头的图片,通常用于默认的三角样式不满足需求时。当使用了 arrowImage 后,arrowSize 将会被固定为 arrowImage.size。 +/// 当 borderWidth 大于0时,arrowImage 会与所在那一侧的 border 重叠,所以你的切图需要预留一部分 borderWidth 的区域以盖住边框。 +/// 图片必须为箭头向下的方向 +@property(nonatomic, strong, nullable) UIImage *arrowImage UI_APPEARANCE_SELECTOR; + /// 最大宽度(指整个控件的宽度,而不是contentView部分),默认为CGFLOAT_MAX @property(nonatomic, assign) CGFloat maximumWidth UI_APPEARANCE_SELECTOR; /// 最小宽度(指整个控件的宽度,而不是contentView部分),默认为0 @property(nonatomic, assign) CGFloat minimumWidth UI_APPEARANCE_SELECTOR; -/// 最大高度(指整个控件的高度,而不是contentView部分),默认为CGFLOAT_MAX +/// 最大高度(指整个控件的高度,而不是contentView部分),默认为CGFLOAT_MAX,会在布局时被动态修改。 @property(nonatomic, assign) CGFloat maximumHeight UI_APPEARANCE_SELECTOR; /// 最小高度(指整个控件的高度,而不是contentView部分),默认为0 @@ -95,58 +122,99 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) { /// 最终的布局方向(preferLayoutDirection只是期望的方向,但有可能那个方向已经没有剩余空间可摆放控件了,所以会自动变换) @property(nonatomic, assign, readonly) QMUIPopupContainerViewLayoutDirection currentLayoutDirection; +/// 计算布局时期望浮层与目标位置的对齐方式,默认为 QMUIPopupContainerViewLayoutAlignmentCenter,也即浮层和目标位置相对居中。 +/// 对 preferLayoutDirection 为 Above/Below 而言,Leading 表示浮层的左侧与目标位置左边缘对齐,Trailing 表示浮层的右侧与目标位置右边缘对齐。 +/// 对 preferLayoutDirection 为 Left/Right 而言,Leading 表示浮层的顶端与目标位置顶边缘对齐,Trailing 表示浮层的底端与目标位置底边缘对齐。 +/// 如果预期的对齐方式无法被满足时,会根据 usesOppositeLayoutAlignmentIfNeeded 的值来决定备选方案。 +@property(nonatomic, assign) QMUIPopupContainerViewLayoutAlignment preferLayoutAlignment UI_APPEARANCE_SELECTOR; + +/// 表示 preferLayoutAlignment 在极端情况下无法满足调用方设置的值时,应该以什么方式作为备选。 +/// 若当前属性值为 YES,则表示用相反的对齐方式去尝试(例如 preferLayoutAlignment = QMUIPopupContainerViewLayoutAlignmentLeading 则在极端情况下会用 QMUIPopupContainerViewLayoutAlignmentTrailing 作为备选),若当前属性值为 NO 则表示保持对齐方向不变,让浮层的边缘紧贴着 safetyMarginsOfSuperview 即可。 +/// 默认为 YES。 +/// @warning 对 QMUIPopupContainerViewLayoutAlignmentCenter 无意义,因为 QMUIPopupContainerViewLayoutAlignmentCenter 没有所谓的相反概念。 +@property(nonatomic, assign) BOOL usesOppositeLayoutAlignmentIfNeeded UI_APPEARANCE_SELECTOR; + /// 最终布局时箭头距离目标边缘的距离,默认为5 @property(nonatomic, assign) CGFloat distanceBetweenSource UI_APPEARANCE_SELECTOR; -/// 最终布局时与父节点的边缘的临界点,默认为(10, 10, 10, 10) +/// 最终布局时与父节点的边缘的临界点,默认为(10, 10, 10, 10),注意这里的值不需要由业务考虑 safeAreaInsets,内部会自己叠加。 @property(nonatomic, assign) UIEdgeInsets safetyMarginsOfSuperview UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *backgroundColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *highlightedBackgroundColor UI_APPEARANCE_SELECTOR; +/// 允许用一个自定的 view 作为背景,会自动将其 mask 为圆角带箭头的造型,当同时使用 backgroundView 和 arrowImage 时,arrowImage 只作为遮罩使用(也即使用它的造型,不显示它的图片内容)。 +/// 默认为 nil。 +@property(nonatomic, strong, nullable) UIView *backgroundView; + +/// 浮层的背景色,作用区域为箭头+圆角矩形区域,当同时使用 backgroundView 和 backgroundColor 时,backgroundView 会盖在 backgroundColor 上方。 +@property(nonatomic, strong, nullable) UIColor *backgroundColor UI_APPEARANCE_SELECTOR; + +/// 浮层点击 highlighted 时的背景色,作用区域为箭头+圆角矩形区域 +@property(nonatomic, strong, nullable) UIColor *highlightedBackgroundColor UI_APPEARANCE_SELECTOR; /// 当使用方法 2 显示并且打开了 automaticallyHidesWhenUserTap 时,可修改背景遮罩的颜色,默认为 UIColorMask,若非使用方法 2,或者没有打开 automaticallyHidesWhenUserTap,则背景遮罩为透明(可视为不存在背景遮罩) -@property(nonatomic, strong) UIColor *maskViewBackgroundColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *shadowColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *borderColor UI_APPEARANCE_SELECTOR; +@property(nonatomic, strong, nullable) UIColor *maskViewBackgroundColor UI_APPEARANCE_SELECTOR; + +/// 浮层的阴影,默认包含箭头的形状,如果使用了 @c arrowImage 则不包含箭头。当不需要阴影时可将其置为 nil。 +@property(nonatomic, strong, nullable) NSShadow *shadow UI_APPEARANCE_SELECTOR; + +@property(nonatomic, strong, nullable) UIColor *borderColor UI_APPEARANCE_SELECTOR; @property(nonatomic, assign) CGFloat borderWidth UI_APPEARANCE_SELECTOR; @property(nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR; /// 可以是 UINavigationBar、UIToolbar 上的 UIBarButtonItem,或者 UITabBar 上的 UITabBarItem -@property(nonatomic, weak) __kindof UIBarItem *sourceBarItem; +@property(nonatomic, weak, nullable) __kindof UIBarItem *sourceBarItem; -@property(nonatomic, weak) __kindof UIView *sourceView; +@property(nonatomic, weak, nullable) __kindof UIView *sourceView; /// rect 需要处于 QMUIPopupContainerView 所在的坐标系内,例如如果 popup 使用 addSubview: 的方式添加到界面,则 sourceRect 应该是 superview 坐标系内的;如果 popup 使用 window 的方式展示,则 sourceRect 需要转换为 window 坐标系内。 @property(nonatomic, assign) CGRect sourceRect; -/// 立即刷新当前 popup 的布局,前提是 popup 已经被 show 过。 +/// 标记为需要更新布局,会在下一次 runloop 里统一调用 updateLayout。一般情况请用这个方法,避免直接用 updateLayout,从而获取更佳的性能。 +- (void)setNeedsUpdateLayout; + +/// 立即刷新当前 popup 的布局,前提是 popup.isShowing 为 YES。 - (void)updateLayout; - (void)showWithAnimated:(BOOL)animated; -- (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion; +- (void)showWithAnimated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion; - (void)hideWithAnimated:(BOOL)animated; -- (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion; +- (void)hideWithAnimated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion; - (BOOL)isShowing; +/// 允许业务自定义显示动画,请在这个 block 里写自己的动画实现,并在恰当的时候调用参数里的 animation() 和 completion()。 +/// @param defaultAnimation 组件里默认的显示动画(默认实现是 scale+alpha),业务可自行决定是否要调用它 +/// @param completion 动画结束的回调,业务必须在自己动画结束时主动调用它 +/// @param isWindowMode 是否正在以 window 模式展示 +/// @param rootView popup 组件当前所在的容器,如果是 window 模式,则是内部自己创建的 window.rootViewController.view,若是其他模式则是业务的 view +/// @param popup 当前 popup 实例 +@property(nonatomic, copy) void (^showingAnimationBlock)(void (^defaultAnimation)(void), void (^completion)(BOOL finished), BOOL isWindowMode, UIView * _Nullable rootView, __kindof QMUIPopupContainerView *popup); + +/// 允许业务自定义隐藏动画,请在这个 block 里写自己的动画实现,并在恰当的时候调用参数里的 animation() 和 completion()。 +/// @param defaultAnimation 组件里默认的显示动画(默认实现是 scale+alpha),业务可自行决定是否要调用它 +/// @param completion 动画结束的回调,业务必须在自己动画结束时主动调用它 +/// @param isWindowMode 是否正在以 window 模式展示 +/// @param rootView popup 组件当前所在的容器,如果是 window 模式,则是内部自己创建的 window.rootViewController.view,若是其他模式则是业务的 view +/// @param popup 当前 popup 实例 +@property(nonatomic, copy) void (^hidingAnimationBlock)(void (^defaultAnimation)(void), void (^completion)(BOOL finished), BOOL isWindowMode, UIView * _Nullable rootView, __kindof QMUIPopupContainerView *popup); + /** * 即将显示时的回调 * 注:如果需要使用例如 didShowBlock 的时机,请使用 @showWithAnimated:completion: 的 completion 参数来实现。 * @argv animated 是否需要动画 */ -@property(nonatomic, copy) void (^willShowBlock)(BOOL animated); +@property(nonatomic, copy, nullable) void (^willShowBlock)(BOOL animated); /** * 即将隐藏时的回调 * @argv hidesByUserTap 用于区分此次隐藏是否因为用户手动点击空白区域导致浮层被隐藏 * @argv animated 是否需要动画 */ -@property(nonatomic, copy) void (^willHideBlock)(BOOL hidesByUserTap, BOOL animated); +@property(nonatomic, copy, nullable) void (^willHideBlock)(BOOL hidesByUserTap, BOOL animated); /** * 已经隐藏后的回调 * @argv hidesByUserTap 用于区分此次隐藏是否因为用户手动点击空白区域导致浮层被隐藏 */ -@property(nonatomic, copy) void (^didHideBlock)(BOOL hidesByUserTap); +@property(nonatomic, copy, nullable) void (^didHideBlock)(BOOL hidesByUserTap); @end @@ -156,10 +224,13 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) { - (void)didInitialize NS_REQUIRES_SUPER; /** - 子类重写,告诉父类subviews的合适大小 + 子类重写,用于告诉父类添加到 contentView 上的 subviews 的大小 @param size 浮层里除去 safetyMarginsOfSuperview、arrowSize、contentEdgeInsets 之外后,留给内容的实际大小,计算 subview 大小时均应使用这个参数来计算 @return 自定义内容实际占据的大小 + @note 计算结果不需要操心 maximumWidth、minimumWidth,这些会由组件统一处理,你只需要在这个方法里返回内容的实际大小即可。 */ - (CGSize)sizeThatFitsInContentView:(CGSize)size; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.m b/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.m index 9d5a9d83..dd76738c 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIPopupContainerView.m @@ -21,6 +21,9 @@ #import "UIView+QMUI.h" #import "UIWindow+QMUI.h" #import "UIBarItem+QMUI.h" +#import "QMUIAppearance.h" +#import "CALayer+QMUI.h" +#import "NSShadow+QMUI.h" @interface QMUIPopupContainerViewWindow : UIWindow @@ -35,14 +38,13 @@ @interface QMUIPopContainerMaskControl : UIControl @property(nonatomic, weak) QMUIPopupContainerView *popupContainerView; @end -@interface QMUIPopupContainerView (UIAppearance) - -- (void)updateAppearance; -@end - @interface QMUIPopupContainerView () { UIImageView *_imageView; UILabel *_textLabel; + + CALayer *_backgroundViewMaskLayer; + CAShapeLayer *_copiedBackgroundLayer; + CALayer *_copiedArrowImageLayer; } @property(nonatomic, strong) QMUIPopupContainerViewWindow *popupWindow; @@ -98,9 +100,40 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { return result; } +- (void)setBackgroundView:(UIView *)backgroundView { + if (_backgroundView && _backgroundView != backgroundView) { + [_backgroundView removeFromSuperview]; + } + _backgroundView = backgroundView; + if (backgroundView) { + [self insertSubview:backgroundView atIndex:0]; + // backgroundView 必须盖在 _backgroundLayer、_arrowImageView 上面,否则背景色、阴影、箭头图片都会盖在 backgroundView 上方,影响表现 + [self sendSubviewToBack:_arrowImageView]; + [self.layer qmui_sendSublayerToBack:_backgroundLayer]; + [self.layer qmui_sendSublayerToBack:_borderLayer]; + if (!_backgroundViewMaskLayer) { + _copiedBackgroundLayer = [CAShapeLayer layer]; + [_copiedBackgroundLayer qmui_removeDefaultAnimations]; + _copiedBackgroundLayer.fillColor = UIColor.blackColor.CGColor;// 这个 layer 是作为 mask 使用的,所以必须完整填充不透明的颜色,否则会影响 mask 效果 + + _copiedArrowImageLayer = [CALayer layer]; + [_copiedArrowImageLayer qmui_removeDefaultAnimations]; + + _backgroundViewMaskLayer = [CALayer layer]; + [_backgroundViewMaskLayer qmui_removeDefaultAnimations]; + [_backgroundViewMaskLayer addSublayer:_copiedBackgroundLayer]; + [_backgroundViewMaskLayer addSublayer:_copiedArrowImageLayer]; + } + backgroundView.layer.mask = _backgroundViewMaskLayer; + } + // 存在 backgroundView 则隐藏原始的箭头,避免在 backgroundView 背后影响显示 + _arrowImageView.hidden = backgroundView || !self.arrowImage; +} + - (void)setBackgroundColor:(UIColor *)backgroundColor { _backgroundColor = backgroundColor; _backgroundLayer.fillColor = _backgroundColor.CGColor; + _arrowImageView.tintColor = backgroundColor; } - (void)setMaskViewBackgroundColor:(UIColor *)maskViewBackgroundColor { @@ -110,19 +143,19 @@ - (void)setMaskViewBackgroundColor:(UIColor *)maskViewBackgroundColor { } } -- (void)setShadowColor:(UIColor *)shadowColor { - _shadowColor = shadowColor; - _backgroundLayer.shadowColor = shadowColor.CGColor; +- (void)setShadow:(NSShadow *)shadow { + _shadow = shadow; + _borderLayer.qmui_shadow = shadow; } - (void)setBorderColor:(UIColor *)borderColor { _borderColor = borderColor; - _backgroundLayer.strokeColor = borderColor.CGColor; + _borderLayer.strokeColor = borderColor.CGColor; } - (void)setBorderWidth:(CGFloat)borderWidth { _borderWidth = borderWidth; - _backgroundLayer.lineWidth = _borderWidth; + _borderLayer.lineWidth = _borderWidth; } - (void)setCornerRadius:(CGFloat)cornerRadius { @@ -133,104 +166,236 @@ - (void)setCornerRadius:(CGFloat)cornerRadius { - (void)setHighlighted:(BOOL)highlighted { [super setHighlighted:highlighted]; if (self.highlightedBackgroundColor) { - _backgroundLayer.fillColor = highlighted ? self.highlightedBackgroundColor.CGColor : self.backgroundColor.CGColor; + UIColor *color = highlighted ? self.highlightedBackgroundColor : self.backgroundColor; + _backgroundLayer.fillColor = color.CGColor; + _arrowImageView.tintColor = color; + } +} + +- (void)setPreferLayoutAlignment:(QMUIPopupContainerViewLayoutAlignment)preferLayoutAlignment { + _preferLayoutAlignment = preferLayoutAlignment; + if (self.isShowing) { + [self setNeedsUpdateLayout]; + } +} + +- (void)setDistanceBetweenSource:(CGFloat)distanceBetweenSource { + _distanceBetweenSource = distanceBetweenSource; + if (self.isShowing) { + [self setNeedsUpdateLayout]; } } - (CGSize)sizeThatFits:(CGSize)size { - size.width = MIN(size.width, CGRectGetWidth(self.superview.bounds) - UIEdgeInsetsGetHorizontalValue(self.safetyMarginsOfSuperview)); - size.height = MIN(size.height, CGRectGetHeight(self.superview.bounds) - UIEdgeInsetsGetVerticalValue(self.safetyMarginsOfSuperview)); - CGSize contentLimitSize = [self contentSizeInSize:size]; - CGSize contentSize = [self sizeThatFitsInContentView:contentLimitSize]; + CGSize contentSize = CGSizeZero; + if (self.contentViewSizeThatFitsBlock) { + contentSize = self.contentViewSizeThatFitsBlock(contentLimitSize); + } else { + contentSize = [self sizeThatFitsInContentView:contentLimitSize]; + } CGSize resultSize = [self sizeWithContentSize:contentSize sizeThatFits:size]; return resultSize; } - (void)layoutSubviews { [super layoutSubviews]; - CGSize arrowSize = self.arrowSize; - CGRect roundedRect = CGRectMake(self.borderWidth / 2.0, self.borderWidth / 2.0 + (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove ? 0 : arrowSize.height), CGRectGetWidth(self.bounds) - self.borderWidth, CGRectGetHeight(self.bounds) - arrowSize.height - self.borderWidth); - CGFloat cornerRadius = self.cornerRadius; + BOOL isUsingArrowImage = !!self.arrowImage; + CGAffineTransform arrowImageTransform = CGAffineTransformIdentity; + CGPoint arrowImagePosition = CGPointZero; + CGSize arrowSize = self.arrowSizeAuto; + if (isUsingArrowImage) { + switch (self.currentLayoutDirection) { + case QMUIPopupContainerViewLayoutDirectionRight: { + arrowImageTransform = CGAffineTransformMakeRotation(AngleWithDegrees(90)); + arrowImagePosition = CGPointMake(arrowSize.width / 2, _arrowMinY + arrowSize.height / 2); + } + break; + case QMUIPopupContainerViewLayoutDirectionAbove: { + arrowImagePosition = CGPointMake(_arrowMinX + arrowSize.width / 2, CGRectGetHeight(self.bounds) - arrowSize.height / 2); + } + break; + case QMUIPopupContainerViewLayoutDirectionLeft: { + arrowImageTransform = CGAffineTransformMakeRotation(AngleWithDegrees(-90)); + arrowImagePosition = CGPointMake(CGRectGetWidth(self.bounds) - arrowSize.width / 2, _arrowMinY + arrowSize.height / 2); + } + break; + case QMUIPopupContainerViewLayoutDirectionBelow: { + arrowImageTransform = CGAffineTransformMakeRotation(AngleWithDegrees(-180)); + arrowImagePosition = CGPointMake(_arrowMinX + arrowSize.width / 2, arrowSize.height / 2); + } + break; + default: + break; + } + _arrowImageView.transform = arrowImageTransform; + _arrowImageView.center = arrowImagePosition; + } + + UIBezierPath *borderPath = [self generatePathForBorder:YES]; + _borderLayer.path = borderPath.CGPath; + _borderLayer.shadowPath = borderPath.CGPath; + _borderLayer.frame = self.bounds; + + UIBezierPath *backgroundPath = [self generatePathForBorder:NO]; + _backgroundLayer.path = backgroundPath.CGPath; + _backgroundLayer.frame = self.bounds; + + if (self.backgroundView) { + self.backgroundView.frame = self.bounds; + _backgroundViewMaskLayer.frame = self.bounds; + + _copiedBackgroundLayer.path = _backgroundLayer.path; + _copiedBackgroundLayer.frame = _backgroundLayer.frame; + + _copiedArrowImageLayer.bounds = _arrowImageView.bounds; + _copiedArrowImageLayer.affineTransform = arrowImageTransform; + _copiedArrowImageLayer.position = arrowImagePosition; + _copiedArrowImageLayer.contents = (id)_arrowImageView.image.CGImage; + _copiedArrowImageLayer.contentsScale = _arrowImageView.image.scale; + } + + [self layoutDefaultSubviews]; +} + +- (void)layoutDefaultSubviews { + self.contentView.frame = CGRectMake( + self.contentEdgeInsets.left + (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight ? self.arrowSizeAuto.width : self.borderWidth), + self.contentEdgeInsets.top + (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow ? self.arrowSizeAuto.height : self.borderWidth), + CGRectGetWidth(self.bounds) - self.borderWidth - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) - self.arrowSpacingInHorizontal - (self.arrowSpacingInHorizontal > 0 ? 0 : self.borderWidth), + CGRectGetHeight(self.bounds) - self.borderWidth - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) - self.arrowSpacingInVertical - (self.arrowSpacingInVertical > 0 ? 0 : self.borderWidth)); + // 让点击响应区域与肉眼看到的圆角矩形保持一致,否则 contentView 内部的 subviews 就算要扩大点击区域也会受限制 + self.contentView.qmui_outsideEdge = UIEdgeInsetsMake(MIN(0, -self.contentEdgeInsets.top), MIN(0, -self.contentEdgeInsets.left), MIN(0, -self.contentEdgeInsets.bottom), MIN(0, -self.contentEdgeInsets.right)); + // contentView的圆角取一个比整个path的圆角小的最大值(极限情况下如果self.contentEdgeInsets.left比self.cornerRadius还大,那就意味着contentView不需要圆角了) + // 这么做是为了尽量去掉contentView对内容不必要的裁剪,以免有些东西被裁剪了看不到 + CGFloat contentViewCornerRadius = fabs(MIN(CGRectGetMinX(self.contentView.frame) - self.cornerRadius, 0)); + self.contentView.layer.cornerRadius = contentViewCornerRadius; + + BOOL isImageViewShowing = [self isSubviewShowing:_imageView]; + BOOL isTextLabelShowing = [self isSubviewShowing:_textLabel]; + if (isImageViewShowing) { + [_imageView sizeToFit]; + _imageView.frame = CGRectSetX(_imageView.frame, self.imageEdgeInsets.left);//, self.imageEdgeInsets.top + (self.contentMode == UIViewContentModeTop ? 0 : CGFloatGetCenter(CGRectGetHeight(self.contentView.bounds), CGRectGetHeight(_imageView.frame)))); + if (self.contentMode == UIViewContentModeTop) { + _imageView.frame = CGRectSetY(_imageView.frame, self.imageEdgeInsets.top); + } else if (self.contentMode == UIViewContentModeBottom) { + _imageView.frame = CGRectSetY(_imageView.frame, CGRectGetHeight(self.contentView.bounds) - self.imageEdgeInsets.bottom - CGRectGetHeight(_imageView.frame)); + } else { + _imageView.frame = CGRectSetY(_imageView.frame, self.imageEdgeInsets.top + CGFloatGetCenter(CGRectGetHeight(self.contentView.bounds) - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets), CGRectGetHeight(_imageView.frame))); + } + } + if (isTextLabelShowing) { + CGFloat textLabelMinX = (isImageViewShowing ? ceil(CGRectGetMaxX(_imageView.frame) + self.imageEdgeInsets.right) : 0) + self.textEdgeInsets.left; + CGSize textLabelLimitSize = CGSizeMake(ceil(CGRectGetWidth(self.contentView.bounds) - textLabelMinX - self.textEdgeInsets.right), ceil(CGRectGetHeight(self.contentView.bounds) - UIEdgeInsetsGetVerticalValue(self.textEdgeInsets))); + CGSize textLabelSize = [_textLabel sizeThatFits:textLabelLimitSize]; + _textLabel.frame = CGRectMake(textLabelMinX, 0, textLabelLimitSize.width, ceil(textLabelSize.height)); + if (self.contentMode == UIViewContentModeTop) { + _textLabel.frame = CGRectSetY(_textLabel.frame, self.textEdgeInsets.top); + } else if (self.contentMode == UIViewContentModeBottom) { + _textLabel.frame = CGRectSetY(_textLabel.frame, CGRectGetHeight(self.contentView.bounds) - self.textEdgeInsets.bottom - CGRectGetHeight(_textLabel.frame)); + } else { + _textLabel.frame = CGRectSetY(_textLabel.frame, self.textEdgeInsets.top + CGFloatGetCenter(CGRectGetHeight(self.contentView.bounds) - UIEdgeInsetsGetVerticalValue(self.textEdgeInsets), CGRectGetHeight(_textLabel.frame))); + } + } +} + +- (UIBezierPath *)generatePathForBorder:(BOOL)forBorder { + BOOL isUsingArrowImage = !!self.arrowImage; + CGSize arrowSize = self.arrowSizeAuto; + CGFloat offset = forBorder ? self.borderWidth / 2.0 : self.borderWidth; + CGRect roundedRect = CGRectMake(offset + (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight ? arrowSize.width - self.borderWidth : 0), + offset + (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow ? arrowSize.height - self.borderWidth : 0), + CGRectGetWidth(self.bounds) - offset * 2 - self.arrowSpacingInHorizontal + (self.arrowSpacingInHorizontal > 0 ? self.borderWidth : 0), + CGRectGetHeight(self.bounds) - offset * 2 - self.arrowSpacingInVertical + (self.arrowSpacingInVertical > 0 ? self.borderWidth : 0)); + CGFloat cornerRadius = forBorder ? self.cornerRadius : (self.cornerRadius - self.borderWidth / 2.0); CGPoint leftTopArcCenter = CGPointMake(CGRectGetMinX(roundedRect) + cornerRadius, CGRectGetMinY(roundedRect) + cornerRadius); CGPoint leftBottomArcCenter = CGPointMake(leftTopArcCenter.x, CGRectGetMaxY(roundedRect) - cornerRadius); CGPoint rightTopArcCenter = CGPointMake(CGRectGetMaxX(roundedRect) - cornerRadius, leftTopArcCenter.y); CGPoint rightBottomArcCenter = CGPointMake(rightTopArcCenter.x, leftBottomArcCenter.y); + // 从左上角逆时针绘制 UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(leftTopArcCenter.x, CGRectGetMinY(roundedRect))]; [path addArcWithCenter:leftTopArcCenter radius:cornerRadius startAngle:M_PI * 1.5 endAngle:M_PI clockwise:NO]; + + if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight) { + // 箭头向左 + if (!isUsingArrowImage) { + [path addLineToPoint:CGPointMake(CGRectGetMinX(roundedRect), _arrowMinY)]; + [path addLineToPoint:CGPointMake(CGRectGetMinX(roundedRect) - arrowSize.width, _arrowMinY + arrowSize.height / 2)]; + [path addLineToPoint:CGPointMake(CGRectGetMinX(roundedRect), _arrowMinY + arrowSize.height)]; + } + } + [path addLineToPoint:CGPointMake(CGRectGetMinX(roundedRect), leftBottomArcCenter.y)]; [path addArcWithCenter:leftBottomArcCenter radius:cornerRadius startAngle:M_PI endAngle:M_PI * 0.5 clockwise:NO]; if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove) { - // 让开,我要开始开始画三角形了,箭头向下 - [path addLineToPoint:CGPointMake(_arrowMinX, CGRectGetMaxY(roundedRect))]; - [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width / 2, CGRectGetMaxY(roundedRect) + arrowSize.height)]; - [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width, CGRectGetMaxY(roundedRect))]; + // 箭头向下 + if (!isUsingArrowImage) { + [path addLineToPoint:CGPointMake(_arrowMinX, CGRectGetMaxY(roundedRect))]; + [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width / 2, CGRectGetMaxY(roundedRect) + arrowSize.height)]; + [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width, CGRectGetMaxY(roundedRect))]; + } } [path addLineToPoint:CGPointMake(rightBottomArcCenter.x, CGRectGetMaxY(roundedRect))]; [path addArcWithCenter:rightBottomArcCenter radius:cornerRadius startAngle:M_PI * 0.5 endAngle:0.0 clockwise:NO]; + + if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionLeft) { + // 箭头向右 + if (!isUsingArrowImage) { + [path addLineToPoint:CGPointMake(CGRectGetMaxX(roundedRect), _arrowMinY + arrowSize.height)]; + [path addLineToPoint:CGPointMake(CGRectGetMaxX(roundedRect) + arrowSize.width, _arrowMinY + arrowSize.height / 2)]; + [path addLineToPoint:CGPointMake(CGRectGetMaxX(roundedRect), _arrowMinY)]; + } + } + [path addLineToPoint:CGPointMake(CGRectGetMaxX(roundedRect), rightTopArcCenter.y)]; [path addArcWithCenter:rightTopArcCenter radius:cornerRadius startAngle:0.0 endAngle:M_PI * 1.5 clockwise:NO]; if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow) { // 箭头向上 - [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width, CGRectGetMinY(roundedRect))]; - [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width / 2, CGRectGetMinY(roundedRect) - arrowSize.height)]; - [path addLineToPoint:CGPointMake(_arrowMinX, CGRectGetMinY(roundedRect))]; + if (!isUsingArrowImage) { + [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width, CGRectGetMinY(roundedRect))]; + [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width / 2, CGRectGetMinY(roundedRect) - arrowSize.height)]; + [path addLineToPoint:CGPointMake(_arrowMinX, CGRectGetMinY(roundedRect))]; + } } [path closePath]; - - _backgroundLayer.path = path.CGPath; - _backgroundLayer.shadowPath = path.CGPath; - _backgroundLayer.frame = self.bounds; - - [self layoutDefaultSubviews]; -} - -- (void)layoutDefaultSubviews { - self.contentView.frame = CGRectMake(self.borderWidth + self.contentEdgeInsets.left, (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove ? self.borderWidth : self.arrowSize.height + self.borderWidth) + self.contentEdgeInsets.top, CGRectGetWidth(self.bounds) - self.borderWidth * 2 - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets), CGRectGetHeight(self.bounds) - self.arrowSize.height - self.borderWidth * 2 - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets)); - // contentView的圆角取一个比整个path的圆角小的最大值(极限情况下如果self.contentEdgeInsets.left比self.cornerRadius还大,那就意味着contentView不需要圆角了) - // 这么做是为了尽量去掉contentView对内容不必要的裁剪,以免有些东西被裁剪了看不到 - CGFloat contentViewCornerRadius = fabs(MIN(CGRectGetMinX(self.contentView.frame) - self.cornerRadius, 0)); - self.contentView.layer.cornerRadius = contentViewCornerRadius; - - BOOL isImageViewShowing = [self isSubviewShowing:_imageView]; - BOOL isTextLabelShowing = [self isSubviewShowing:_textLabel]; - if (isImageViewShowing) { - [_imageView sizeToFit]; - _imageView.frame = CGRectSetXY(_imageView.frame, self.imageEdgeInsets.left, CGFloatGetCenter(CGRectGetHeight(self.contentView.bounds), CGRectGetHeight(_imageView.frame) + self.imageEdgeInsets.top)); - } - if (isTextLabelShowing) { - CGFloat textLabelMinX = (isImageViewShowing ? ceil(CGRectGetMaxX(_imageView.frame) + self.imageEdgeInsets.right) : 0) + self.textEdgeInsets.left; - CGSize textLabelLimitSize = CGSizeMake(ceil(CGRectGetWidth(self.contentView.bounds) - textLabelMinX), ceil(CGRectGetHeight(self.contentView.bounds) - self.textEdgeInsets.top - self.textEdgeInsets.bottom)); - CGSize textLabelSize = [_textLabel sizeThatFits:textLabelLimitSize]; - CGPoint textLabelOrigin = CGPointMake(textLabelMinX, flat(CGFloatGetCenter(CGRectGetHeight(self.contentView.bounds), ceil(textLabelSize.height)) + self.textEdgeInsets.top)); - _textLabel.frame = CGRectMake(textLabelOrigin.x, textLabelOrigin.y, textLabelLimitSize.width, ceil(textLabelSize.height)); - } + return path; } - (void)setSourceBarItem:(__kindof UIBarItem *)sourceBarItem { + if (_sourceBarItem && _sourceBarItem != sourceBarItem) { + _sourceBarItem.qmui_viewLayoutDidChangeBlock = nil; + } + _sourceBarItem = sourceBarItem; + if (!_sourceBarItem) return; + __weak __typeof(self)weakSelf = self; - if (!sourceBarItem.qmui_viewLayoutDidChangeBlock) { - sourceBarItem.qmui_viewLayoutDidChangeBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) { - if (!view.window || !weakSelf.superview) return; - UIView *convertToView = weakSelf.popupWindow ? UIApplication.sharedApplication.delegate.window : weakSelf.superview;// 对于以 window 方式显示的情况,由于横竖屏旋转时,不同 window 的旋转顺序不同,所以可能导致 sourceBarItem 所在的 window 已经旋转了但 popupWindow 还没旋转(iOS 11 及以后),那么计算出来的坐标就错了,所以这里改为用 UIApplication window - CGRect rect = [view qmui_convertRect:view.bounds toView:convertToView]; - weakSelf.sourceRect = rect; - }; - } + // 每次都要重新定义 block,否则当不同的 popup 在同一个 sourceBarItem 显示,这个 block 内部得到的 weakSelf 可能是前一次的 + sourceBarItem.qmui_viewLayoutDidChangeBlock = ^(__kindof UIBarItem * _Nonnull item, UIView * _Nullable view) { + if (!view.window || !weakSelf.superview) return; + UIView *convertToView = weakSelf.popupWindow ? UIApplication.sharedApplication.delegate.window : weakSelf.superview;// 对于以 window 方式显示的情况,由于横竖屏旋转时,不同 window 的旋转顺序不同,所以可能导致 sourceBarItem 所在的 window 已经旋转了但 popupWindow 还没旋转(iOS 11 及以后),那么计算出来的坐标就错了,所以这里改为用 UIApplication window + CGRect rect = [view qmui_convertRect:view.bounds toView:convertToView]; + weakSelf.sourceRect = rect; + }; if (sourceBarItem.qmui_view && sourceBarItem.qmui_viewLayoutDidChangeBlock) { sourceBarItem.qmui_viewLayoutDidChangeBlock(sourceBarItem, sourceBarItem.qmui_view);// update layout immediately } } - (void)setSourceView:(__kindof UIView *)sourceView { + if (_sourceView && _sourceView != sourceView) { + _sourceView.qmui_frameDidChangeBlock = nil; + } + _sourceView = sourceView; + if (!_sourceView) return; + __weak __typeof(self)weakSelf = self; sourceView.qmui_frameDidChangeBlock = ^(__kindof UIView * _Nonnull view, CGRect precedingFrame) { if (!view.window || !weakSelf.superview) return; @@ -248,6 +413,16 @@ - (void)setSourceRect:(CGRect)sourceRect { } } +- (void)setNeedsUpdateLayout { + if (_shouldInvalidateLayout) return; + _shouldInvalidateLayout = YES; + dispatch_async(dispatch_get_main_queue(), ^{ + if (self->_shouldInvalidateLayout) { + [self updateLayout]; + } + }); +} + - (void)updateLayout { // call setter to layout immediately if (self.sourceBarItem) { @@ -257,6 +432,7 @@ - (void)updateLayout { } else { self.sourceRect = self.sourceRect; } + _shouldInvalidateLayout = NO; } // 参数 targetRect 在 window 模式下是 window 的坐标系内的,如果是 subview 模式下则是 superview 坐标系内的 @@ -266,77 +442,195 @@ - (void)layoutWithTargetRect:(CGRect)targetRect { return; } + _currentLayoutDirection = self.preferLayoutDirection; targetRect = self.popupWindow ? [self.popupWindow convertRect:targetRect toView:superview] : targetRect; CGRect containerRect = superview.bounds; - CGSize tipSize = [self sizeThatFits:CGSizeMake(self.maximumWidth, self.maximumHeight)]; - CGFloat preferredTipWidth = tipSize.width; - - // 保护tips最往左只能到达self.safetyMarginsOfSuperview.left - CGFloat a = CGRectGetMidX(targetRect) - tipSize.width / 2; - CGFloat tipMinX = MAX(CGRectGetMinX(containerRect) + self.safetyMarginsOfSuperview.left, a); - - CGFloat tipMaxX = tipMinX + tipSize.width; - if (tipMaxX + self.safetyMarginsOfSuperview.right > CGRectGetMaxX(containerRect)) { - // 右边超出了 - // 先尝试把右边超出的部分往左边挪,看是否会令左边到达临界点 - CGFloat distanceCanMoveToLeft = tipMaxX - (CGRectGetMaxX(containerRect) - self.safetyMarginsOfSuperview.right); - if (tipMinX - distanceCanMoveToLeft >= CGRectGetMinX(containerRect) + self.safetyMarginsOfSuperview.left) { - // 可以往左边挪 - tipMinX -= distanceCanMoveToLeft; - } else { - // 不可以往左边挪,那么让左边靠到临界点,然后再把宽度减小,以让右边处于临界点以内 - tipMinX = CGRectGetMinX(containerRect) + self.safetyMarginsOfSuperview.left; - tipMaxX = CGRectGetMaxX(containerRect) - self.safetyMarginsOfSuperview.right; - tipSize.width = MIN(tipSize.width, tipMaxX - tipMinX); + CGSize (^sizeToFitBlock)(void) = ^CGSize(void) { + CGSize result = CGSizeZero; + if (self.isVerticalLayoutDirection) { + result.width = CGRectGetWidth(containerRect) - UIEdgeInsetsGetHorizontalValue(self.safetyMarginsAvoidSafeAreaInsets); + } else if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionLeft) { + result.width = CGRectGetMinX(targetRect) - self.distanceBetweenSource - self.safetyMarginsAvoidSafeAreaInsets.left; + } else if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight) { + result.width = CGRectGetWidth(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.right - self.distanceBetweenSource - CGRectGetMaxX(targetRect); } - } + if (self.isHorizontalLayoutDirection) { + result.height = CGRectGetHeight(containerRect) - UIEdgeInsetsGetVerticalValue(self.safetyMarginsAvoidSafeAreaInsets); + } else if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove) { + result.height = CGRectGetMinY(targetRect) - self.distanceBetweenSource - self.safetyMarginsAvoidSafeAreaInsets.top; + } else if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow) { + result.height = CGRectGetHeight(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.bottom - self.distanceBetweenSource - CGRectGetMaxY(targetRect); + } + result = CGSizeMake(MIN(self.maximumWidth, result.width), MIN(self.maximumHeight, result.height)); + return result; + }; - // 经过上面一番调整,可能tipSize.width发生变化,一旦宽度变化,高度要重新计算,所以重新调用一次sizeThatFits - BOOL tipWidthChanged = tipSize.width != preferredTipWidth; - if (tipWidthChanged) { - tipSize = [self sizeThatFits:tipSize]; - } - _currentLayoutDirection = self.preferLayoutDirection; + CGSize tipSize = [self sizeThatFits:sizeToFitBlock()]; + CGFloat preferredTipWidth = tipSize.width; + CGFloat preferredTipHeight = tipSize.height; + CGFloat tipMinX = 0; + CGFloat tipMinY = 0; - // 检查当前的最大高度是否超过任一方向的剩余空间,如果是,则强制减小最大高度,避免后面计算布局选择方向时死循环 - BOOL canShowAtAbove = [self canTipShowAtSpecifiedLayoutDirect:QMUIPopupContainerViewLayoutDirectionAbove targetRect:targetRect tipSize:tipSize]; - BOOL canShowAtBelow = [self canTipShowAtSpecifiedLayoutDirect:QMUIPopupContainerViewLayoutDirectionBelow targetRect:targetRect tipSize:tipSize]; - - if (!canShowAtAbove && !canShowAtBelow) { - // 上下都没有足够的空间,所以要调整maximumHeight - CGFloat maximumHeightAbove = CGRectGetMinY(targetRect) - CGRectGetMinY(containerRect) - self.distanceBetweenSource - self.safetyMarginsOfSuperview.top; - CGFloat maximumHeightBelow = CGRectGetMaxY(containerRect) - self.safetyMarginsOfSuperview.bottom - self.distanceBetweenSource - CGRectGetMaxY(targetRect); - self.maximumHeight = MAX(self.minimumHeight, MAX(maximumHeightAbove, maximumHeightBelow)); - tipSize.height = self.maximumHeight; - _currentLayoutDirection = maximumHeightAbove > maximumHeightBelow ? QMUIPopupContainerViewLayoutDirectionAbove : QMUIPopupContainerViewLayoutDirectionBelow; + if (self.isVerticalLayoutDirection) { + // 保护tips最往左只能到达self.safetyMarginsAvoidSafeAreaInsets.left + CGFloat a = 0; + switch (self.preferLayoutAlignment) { + case QMUIPopupContainerViewLayoutAlignmentLeading: + a = CGRectGetMinX(targetRect); + break; + case QMUIPopupContainerViewLayoutAlignmentTrailing: + a = CGRectGetMaxX(targetRect) - tipSize.width; + break; + default: + a = CGRectGetMidX(targetRect) - tipSize.width / 2; + break; + } + tipMinX = MAX(CGRectGetMinX(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.left, a); - QMUILog(NSStringFromClass(self.class), @"%@, 因为上下都不够空间,所以最大高度被强制改为%@, 位于目标的%@", self, @(self.maximumHeight), maximumHeightAbove > maximumHeightBelow ? @"上方" : @"下方"); + CGFloat tipMaxX = tipMinX + tipSize.width; + if (tipMaxX + self.safetyMarginsAvoidSafeAreaInsets.right > CGRectGetMaxX(containerRect)) { + // 右边超出了 + // 先尝试把右边超出的部分往左边挪,看是否会令左边到达临界点 + CGFloat distanceCanMoveToLeft = 0; + if (self.preferLayoutAlignment == QMUIPopupContainerViewLayoutAlignmentLeading && self.usesOppositeLayoutAlignmentIfNeeded) { + distanceCanMoveToLeft = tipMaxX - MIN(CGRectGetMaxX(targetRect), CGRectGetMaxX(containerRect) - self.safetyMarginsOfSuperview.right);// targetRect 可能溢出屏幕外,需要保护 + if (tipMinX - distanceCanMoveToLeft >= CGRectGetMinX(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.left) { + // 可以往左边挪,走下面的统一逻辑 + } else { + // 不可以往左边挪,那就算了按原始 alignment 来对待 + distanceCanMoveToLeft = tipMaxX - (CGRectGetMaxX(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.right); + } + } else { + distanceCanMoveToLeft = tipMaxX - (CGRectGetMaxX(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.right); + } + if (tipMinX - distanceCanMoveToLeft >= CGRectGetMinX(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.left) { + // 可以往左边挪 + tipMinX -= distanceCanMoveToLeft; + } else { + // 不可以往左边挪,那么让左边靠到临界点,然后再把宽度减小,以让右边处于临界点以内 + tipMinX = CGRectGetMinX(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.left; + tipMaxX = CGRectGetMaxX(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.right; + tipSize.width = MIN(tipSize.width, tipMaxX - tipMinX); + } + } - } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove && !canShowAtAbove) { - _currentLayoutDirection = QMUIPopupContainerViewLayoutDirectionBelow; - } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow && !canShowAtBelow) { - _currentLayoutDirection = QMUIPopupContainerViewLayoutDirectionAbove; - } - - CGFloat tipMinY = [self tipMinYWithTargetRect:targetRect tipSize:tipSize preferLayoutDirection:_currentLayoutDirection]; - - // 当上下的剩余空间都比最小高度要小的时候,tip会靠在safetyMargins范围内的上(下)边缘 - if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove) { - CGFloat tipMinYIfAlignSafetyMarginTop = CGRectGetMinY(containerRect) + self.safetyMarginsOfSuperview.top; - tipMinY = MAX(tipMinY, tipMinYIfAlignSafetyMarginTop); - } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow) { - CGFloat tipMinYIfAlignSafetyMarginBottom = CGRectGetMaxY(containerRect) - self.safetyMarginsOfSuperview.bottom - tipSize.height; - tipMinY = MIN(tipMinY, tipMinYIfAlignSafetyMarginBottom); + // 经过上面一番调整,可能tipSize.width发生变化,一旦宽度变化,高度要重新计算,所以重新调用一次sizeThatFits + BOOL tipWidthChanged = tipSize.width != preferredTipWidth; + if (tipWidthChanged) { + tipSize = [self sizeThatFits:tipSize]; + } + + // 检查当前的最大高度是否超过任一方向的剩余空间,如果是,则强制减小最大高度,避免后面计算布局选择方向时死循环 + BOOL canShowAtAbove = [self canTipShowAtSpecifiedLayoutDirect:QMUIPopupContainerViewLayoutDirectionAbove targetRect:targetRect tipSize:tipSize]; + BOOL canShowAtBelow = [self canTipShowAtSpecifiedLayoutDirect:QMUIPopupContainerViewLayoutDirectionBelow targetRect:targetRect tipSize:tipSize]; + + if (!canShowAtAbove && !canShowAtBelow) { + // 上下都没有足够的空间,所以要调整maximumHeight + CGFloat maximumHeightAbove = CGRectGetMinY(targetRect) - CGRectGetMinY(containerRect) - self.distanceBetweenSource - self.safetyMarginsAvoidSafeAreaInsets.top; + CGFloat maximumHeightBelow = CGRectGetMaxY(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.bottom - self.distanceBetweenSource - CGRectGetMaxY(targetRect); + self.maximumHeight = MAX(self.minimumHeight, MAX(maximumHeightAbove, maximumHeightBelow)); + tipSize.height = self.maximumHeight; + _currentLayoutDirection = maximumHeightAbove > maximumHeightBelow ? QMUIPopupContainerViewLayoutDirectionAbove : QMUIPopupContainerViewLayoutDirectionBelow; + + QMUILog(NSStringFromClass(self.class), @"%@, 因为上下都不够空间,所以最大高度被强制改为%@, 位于目标的%@", self, @(self.maximumHeight), maximumHeightAbove > maximumHeightBelow ? @"上方" : @"下方"); + + } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove && !canShowAtAbove) { + _currentLayoutDirection = QMUIPopupContainerViewLayoutDirectionBelow; + tipSize.height = [self sizeThatFits:CGSizeMake(tipSize.width, sizeToFitBlock().height)].height; + } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow && !canShowAtBelow) { + _currentLayoutDirection = QMUIPopupContainerViewLayoutDirectionAbove; + tipSize.height = [self sizeThatFits:CGSizeMake(tipSize.width, sizeToFitBlock().height)].height; + } + + tipMinY = [self tipOriginWithTargetRect:targetRect tipSize:tipSize preferLayoutDirection:_currentLayoutDirection].y; + + // 当上下的剩余空间都比最小高度要小的时候,tip会靠在safetyMargins范围内的上(下)边缘 + if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove) { + CGFloat tipMinYIfAlignSafetyMarginTop = CGRectGetMinY(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.top; + tipMinY = MAX(tipMinY, tipMinYIfAlignSafetyMarginTop); + } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow) { + CGFloat tipMinYIfAlignSafetyMarginBottom = CGRectGetMaxY(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.bottom - tipSize.height; + tipMinY = MIN(tipMinY, tipMinYIfAlignSafetyMarginBottom); + } + + self.frame = CGRectFlatMake(tipMinX, tipMinY, tipSize.width, tipSize.height); + + // 调整浮层里的箭头的位置 + CGPoint targetRectCenter = CGPointGetCenterWithRect(targetRect); + CGFloat selfMidX = targetRectCenter.x - CGRectGetMinX(self.frame); + CGFloat arrowMinimumMinX = self.cornerRadius; + CGFloat arrowMaximumMinX = CGRectGetWidth(self.bounds) - self.cornerRadius - self.arrowSize.width; + _arrowMinX = MIN(arrowMaximumMinX, MAX(arrowMinimumMinX, selfMidX - self.arrowSizeAuto.width / 2)); + } else { + // 保护tips最往上只能到达self.safetyMarginsAvoidSafeAreaInsets.top + CGFloat a = CGRectGetMidY(targetRect) - tipSize.height / 2; + tipMinY = MAX(CGRectGetMinY(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.top, a); + + CGFloat tipMaxY = tipMinY + tipSize.height; + if (tipMaxY + self.safetyMarginsAvoidSafeAreaInsets.bottom > CGRectGetMaxY(containerRect)) { + // 下面超出了 + // 先尝试把下面超出的部分往上面挪,看是否会令上面到达临界点 + CGFloat distanceCanMoveToTop = tipMaxY - (CGRectGetMaxY(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.bottom); + if (tipMinY - distanceCanMoveToTop >= CGRectGetMinY(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.top) { + // 可以往上面挪 + tipMinY -= distanceCanMoveToTop; + } else { + // 不可以往上面挪,那么让上面靠到临界点,然后再把高度减小,以让下面处于临界点以内 + tipMinY = CGRectGetMinY(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.top; + tipMaxY = CGRectGetMaxY(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.bottom; + tipSize.height = MIN(tipSize.height, tipMaxY - tipMinY); + } + } + + // 经过上面一番调整,可能tipSize.height发生变化,一旦高度变化,高度要重新计算,所以重新调用一次sizeThatFits + BOOL tipHeightChanged = tipSize.height != preferredTipHeight; + if (tipHeightChanged) { + tipSize = [self sizeThatFits:tipSize]; + } + + // 检查当前的最大宽度是否超过任一方向的剩余空间,如果是,则强制减小最大宽度,避免后面计算布局选择方向时死循环 + BOOL canShowAtLeft = [self canTipShowAtSpecifiedLayoutDirect:QMUIPopupContainerViewLayoutDirectionLeft targetRect:targetRect tipSize:tipSize]; + BOOL canShowAtRight = [self canTipShowAtSpecifiedLayoutDirect:QMUIPopupContainerViewLayoutDirectionRight targetRect:targetRect tipSize:tipSize]; + + if (!canShowAtLeft && !canShowAtRight) { + // 左右都没有足够的空间,所以要调整maximumWidth + CGFloat maximumWidthLeft = CGRectGetMinX(targetRect) - CGRectGetMinX(containerRect) - self.distanceBetweenSource - self.safetyMarginsAvoidSafeAreaInsets.left; + CGFloat maximumWidthRight = CGRectGetMaxX(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.right - self.distanceBetweenSource - CGRectGetMaxX(targetRect); + self.maximumWidth = MAX(self.minimumWidth, MAX(maximumWidthLeft, maximumWidthRight)); + tipSize.width = self.maximumWidth; + _currentLayoutDirection = maximumWidthLeft > maximumWidthRight ? QMUIPopupContainerViewLayoutDirectionLeft : QMUIPopupContainerViewLayoutDirectionRight; + + QMUILog(NSStringFromClass(self.class), @"%@, 因为左右都不够空间,所以最大宽度被强制改为%@, 位于目标的%@", self, @(self.maximumWidth), maximumWidthLeft > maximumWidthRight ? @"左边" : @"右边"); + + } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionLeft && !canShowAtLeft) { + _currentLayoutDirection = QMUIPopupContainerViewLayoutDirectionLeft; + tipSize.width = [self sizeThatFits:CGSizeMake(sizeToFitBlock().width, tipSize.height)].width; + } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow && !canShowAtRight) { + _currentLayoutDirection = QMUIPopupContainerViewLayoutDirectionRight; + tipSize.width = [self sizeThatFits:CGSizeMake(sizeToFitBlock().width, tipSize.height)].width; + } + + tipMinX = [self tipOriginWithTargetRect:targetRect tipSize:tipSize preferLayoutDirection:_currentLayoutDirection].x; + + // 当左右的剩余空间都比最小宽度要小的时候,tip会靠在safetyMargins范围内的左(右)边缘 + if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionLeft) { + CGFloat tipMinXIfAlignSafetyMarginLeft = CGRectGetMinX(containerRect) + self.safetyMarginsAvoidSafeAreaInsets.left; + tipMinX = MAX(tipMinX, tipMinXIfAlignSafetyMarginLeft); + } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight) { + CGFloat tipMinXIfAlignSafetyMarginRight = CGRectGetMaxX(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.right - tipSize.width; + tipMinX = MIN(tipMinX, tipMinXIfAlignSafetyMarginRight); + } + + self.frame = CGRectFlatMake(tipMinX, tipMinY, tipSize.width, tipSize.height); + + // 调整浮层里的箭头的位置 + CGPoint targetRectCenter = CGPointGetCenterWithRect(targetRect); + CGFloat selfMidY = targetRectCenter.y - CGRectGetMinY(self.frame); + _arrowMinY = selfMidY - self.arrowSizeAuto.height / 2; } - self.frame = CGRectFlatMake(tipMinX, tipMinY, tipSize.width, tipSize.height); - - // 调整浮层里的箭头的位置 - CGPoint targetRectCenter = CGPointGetCenterWithRect(targetRect); - CGFloat selfMidX = targetRectCenter.x - (CGRectGetMinX(containerRect) + CGRectGetMinX(self.frame)); - _arrowMinX = selfMidX - self.arrowSize.width / 2; [self setNeedsLayout]; if (self.debug) { @@ -348,24 +642,45 @@ - (void)layoutWithTargetRect:(CGRect)targetRect { } } -- (CGFloat)tipMinYWithTargetRect:(CGRect)itemRect tipSize:(CGSize)tipSize preferLayoutDirection:(QMUIPopupContainerViewLayoutDirection)direction { - CGFloat tipMinY = 0; - if (direction == QMUIPopupContainerViewLayoutDirectionAbove) { - tipMinY = CGRectGetMinY(itemRect) - tipSize.height - self.distanceBetweenSource; - } else if (direction == QMUIPopupContainerViewLayoutDirectionBelow) { - tipMinY = CGRectGetMaxY(itemRect) + self.distanceBetweenSource; +- (CGPoint)tipOriginWithTargetRect:(CGRect)itemRect tipSize:(CGSize)tipSize preferLayoutDirection:(QMUIPopupContainerViewLayoutDirection)direction { + CGPoint tipOrigin = CGPointZero; + switch (direction) { + case QMUIPopupContainerViewLayoutDirectionAbove: + tipOrigin.y = CGRectGetMinY(itemRect) - tipSize.height - self.distanceBetweenSource; + break; + case QMUIPopupContainerViewLayoutDirectionBelow: + tipOrigin.y = CGRectGetMaxY(itemRect) + self.distanceBetweenSource; + break; + case QMUIPopupContainerViewLayoutDirectionLeft: + tipOrigin.x = CGRectGetMinX(itemRect) - tipSize.width - self.distanceBetweenSource; + break; + case QMUIPopupContainerViewLayoutDirectionRight: + tipOrigin.x = CGRectGetMaxX(itemRect) + self.distanceBetweenSource; + break; + default: + break; } - return tipMinY; + return tipOrigin; } - (BOOL)canTipShowAtSpecifiedLayoutDirect:(QMUIPopupContainerViewLayoutDirection)direction targetRect:(CGRect)itemRect tipSize:(CGSize)tipSize { BOOL canShow = NO; - CGFloat tipMinY = [self tipMinYWithTargetRect:itemRect tipSize:tipSize preferLayoutDirection:direction]; - if (direction == QMUIPopupContainerViewLayoutDirectionAbove) { - canShow = tipMinY >= self.safetyMarginsOfSuperview.top; - } else if (direction == QMUIPopupContainerViewLayoutDirectionBelow) { - canShow = tipMinY + tipSize.height + self.safetyMarginsOfSuperview.bottom <= CGRectGetHeight(self.superview.bounds); + if (self.isVerticalLayoutDirection) { + CGFloat tipMinY = [self tipOriginWithTargetRect:itemRect tipSize:tipSize preferLayoutDirection:direction].y; + if (direction == QMUIPopupContainerViewLayoutDirectionAbove) { + canShow = tipMinY >= self.safetyMarginsAvoidSafeAreaInsets.top; + } else if (direction == QMUIPopupContainerViewLayoutDirectionBelow) { + canShow = tipMinY + tipSize.height + self.safetyMarginsAvoidSafeAreaInsets.bottom <= CGRectGetHeight(self.superview.bounds); + } + } else { + CGFloat tipMinX = [self tipOriginWithTargetRect:itemRect tipSize:tipSize preferLayoutDirection:direction].x; + if (direction == QMUIPopupContainerViewLayoutDirectionLeft) { + canShow = tipMinX >= self.safetyMarginsAvoidSafeAreaInsets.left; + } else if (direction == QMUIPopupContainerViewLayoutDirectionRight) { + canShow = tipMinX + tipSize.width + self.safetyMarginsAvoidSafeAreaInsets.right <= CGRectGetWidth(self.superview.bounds); + } } + return canShow; } @@ -382,7 +697,7 @@ - (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { QMUICommonViewController *viewController = (QMUICommonViewController *)self.popupWindow.rootViewController; viewController.supportedOrientationMask = [QMUIHelper visibleViewController].supportedInterfaceOrientations; - self.previousKeyWindow = [UIApplication sharedApplication].keyWindow; + self.previousKeyWindow = UIApplication.sharedApplication.keyWindow; [self.popupWindow makeKeyAndVisible]; isShowingByWindowMode = YES; @@ -398,28 +713,43 @@ - (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { if (animated) { if (isShowingByWindowMode) { - self.popupWindow.alpha = 0; + self.popupWindow.rootViewController.view.alpha = 0;// 请操作 vc.view.alpha 而不是 window.alpha,如果是后者,会导致 popup 显示出来前有一小段时间无法屏蔽界面的触摸事件,从而引发一些状态混乱问题 } else { self.alpha = 0; } self.layer.transform = CATransform3DMakeScale(0.98, 0.98, 1); - [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.3 initialSpringVelocity:12 options:UIViewAnimationOptionCurveLinear animations:^{ - self.layer.transform = CATransform3DMakeScale(1, 1, 1); - } completion:^(BOOL finished) { - if (completion) { - completion(finished); - } - }]; - [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{ - if (isShowingByWindowMode) { - self.popupWindow.alpha = 1; - } else { - self.alpha = 1; - } - } completion:nil]; + if (self.showingAnimationBlock) { + self.showingAnimationBlock(^{ + self.layer.transform = CATransform3DMakeScale(1, 1, 1); + if (isShowingByWindowMode) { + self.popupWindow.rootViewController.view.alpha = 1; + } else { + self.alpha = 1; + } + }, ^(BOOL finished) { + if (completion) { + completion(finished); + } + }, isShowingByWindowMode, self.popupWindow.rootViewController.view, self); + } else { + [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.3 initialSpringVelocity:12 options:UIViewAnimationOptionCurveLinear animations:^{ + self.layer.transform = CATransform3DMakeScale(1, 1, 1); + } completion:^(BOOL finished) { + if (completion) { + completion(finished); + } + }]; + [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{ + if (isShowingByWindowMode) { + self.popupWindow.rootViewController.view.alpha = 1; + } else { + self.alpha = 1; + } + } completion:nil]; + } } else { if (isShowingByWindowMode) { - self.popupWindow.alpha = 1; + self.popupWindow.rootViewController.view.alpha = 1; } else { self.alpha = 1; } @@ -441,15 +771,25 @@ - (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { BOOL isShowingByWindowMode = !!self.popupWindow; if (animated) { - [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{ + void (^a)(void) = ^void(void) { if (isShowingByWindowMode) { - self.popupWindow.alpha = 0; + self.popupWindow.rootViewController.view.alpha = 0; } else { self.alpha = 0; } - } completion:^(BOOL finished) { + }; + void (^c)(BOOL finished) = ^void(BOOL finished) { [self hideCompletionWithWindowMode:isShowingByWindowMode completion:completion]; - }]; + }; + if (self.hidingAnimationBlock) { + self.hidingAnimationBlock(a, c, isShowingByWindowMode, self.popupWindow.rootViewController.view, self); + } else { + [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{ + a(); + } completion:^(BOOL finished) { + c(finished); + }]; + } } else { [self hideCompletionWithWindowMode:isShowingByWindowMode completion:completion]; } @@ -458,7 +798,7 @@ - (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { - (void)hideCompletionWithWindowMode:(BOOL)windowMode completion:(void (^)(BOOL))completion { if (windowMode) { // 恢复 keyWindow 之前做一下检查,避免类似问题 https://github.com/Tencent/QMUI_iOS/issues/90 - if ([[UIApplication sharedApplication] keyWindow] == self.popupWindow) { + if (UIApplication.sharedApplication.keyWindow == self.popupWindow) { [self.previousKeyWindow makeKeyWindow]; } @@ -512,36 +852,126 @@ - (void)initPopupContainerViewWindowIfNeeded { } } -/// 根据一个给定的大小,计算出符合这个大小的内容大小 +/// 根据一个给定的大小(包含箭头,不含 distanceBetweenSource ),计算出符合这个大小的内容大小(去掉箭头和白色内部的 contentEdgeInsets 后) - (CGSize)contentSizeInSize:(CGSize)size { - CGSize contentSize = CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) - self.borderWidth * 2, size.height - self.arrowSize.height - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) - self.borderWidth * 2); + CGSize contentSize = CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) - self.borderWidth - self.arrowSpacingInHorizontal - (self.arrowSpacingInHorizontal > 0 ? 0 : self.borderWidth), size.height - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) - self.borderWidth - self.arrowSpacingInVertical - (self.arrowSpacingInVertical > 0 ? 0 : self.borderWidth)); return contentSize; } /// 根据内容大小和外部限制的大小,计算出合适的self size(包含箭头) - (CGSize)sizeWithContentSize:(CGSize)contentSize sizeThatFits:(CGSize)sizeThatFits { - CGFloat resultWidth = contentSize.width + UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) + self.borderWidth * 2; - resultWidth = MIN(resultWidth, sizeThatFits.width);// 宽度不能超过传进来的size.width + CGFloat resultWidth = contentSize.width + UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) + self.borderWidth + self.arrowSpacingInHorizontal + (self.arrowSpacingInHorizontal > 0 ? 0 : self.borderWidth);// 当存在箭头时,箭头会叠在 border 上,所以这里只加一条边 resultWidth = MAX(MIN(resultWidth, self.maximumWidth), self.minimumWidth);// 宽度必须在最小值和最大值之间 - resultWidth = ceil(resultWidth); + resultWidth = flat(resultWidth); - CGFloat resultHeight = contentSize.height + UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) + self.arrowSize.height + self.borderWidth * 2; - resultHeight = MIN(resultHeight, sizeThatFits.height); + CGFloat resultHeight = contentSize.height + UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) + self.borderWidth + self.arrowSpacingInVertical + (self.arrowSpacingInVertical > 0 ? 0 : self.borderWidth);// 当存在箭头时,箭头会叠在 border 上,所以这里只加一条边 resultHeight = MAX(MIN(resultHeight, self.maximumHeight), self.minimumHeight); - resultHeight = ceil(resultHeight); + resultHeight = flat(resultHeight); return CGSizeMake(resultWidth, resultHeight); } +- (BOOL)isHorizontalLayoutDirection { + return self.preferLayoutDirection == QMUIPopupContainerViewLayoutDirectionLeft || self.preferLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight; +} + +- (BOOL)isVerticalLayoutDirection { + return self.preferLayoutDirection == QMUIPopupContainerViewLayoutDirectionAbove || self.preferLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow; +} + +- (void)setArrowImage:(UIImage *)arrowImage { + _arrowImage = arrowImage; + if (arrowImage) { + _arrowSize = arrowImage.size; + + if (!_arrowImageView) { + _arrowImageView = UIImageView.new; + _arrowImageView.tintColor = self.backgroundColor; + [self addSubview:_arrowImageView]; + } + _arrowImageView.hidden = !!self.backgroundView;// 存在 backgroundView 时不要显示箭头(但依然要设置 _arrowImageView 的内容,以供 mask 用) + _arrowImageView.image = arrowImage; + _arrowImageView.bounds = CGRectMakeWithSize(arrowImage.size); + } else { + _arrowImageView.hidden = YES; + _arrowImageView.image = nil; + } +} + +- (void)setArrowSize:(CGSize)arrowSize { + if (!self.arrowImage) { + _arrowSize = arrowSize; + if (self.isShowing) { + [self setNeedsUpdateLayout]; + } + } +} + +// self.arrowSize 规定的是上下箭头的宽高,如果 tip 布局在左右的话,arrowSize 的宽高则调转 +- (CGSize)arrowSizeAuto { + return self.isHorizontalLayoutDirection ? CGSizeMake(self.arrowSize.height, self.arrowSize.width) : self.arrowSize; +} + +- (CGFloat)arrowSpacingInHorizontal { + return self.isHorizontalLayoutDirection ? self.arrowSizeAuto.width : 0; +} + +- (CGFloat)arrowSpacingInVertical { + return self.isVerticalLayoutDirection ? self.arrowSizeAuto.height : 0; +} + +- (void)setMinimumWidth:(CGFloat)minimumWidth { + _minimumWidth = minimumWidth; + if (self.isShowing) { + [self setNeedsUpdateLayout]; + } +} + +- (void)setMaximumWidth:(CGFloat)maximumWidth { + _maximumWidth = maximumWidth; + if (self.isShowing) { + [self setNeedsUpdateLayout]; + } +} + +- (void)setMinimumHeight:(CGFloat)minimumHeight { + _minimumHeight = minimumHeight; + if (self.isShowing) { + [self setNeedsUpdateLayout]; + } +} + +- (void)setMaximumHeight:(CGFloat)maximumHeight { + _maximumHeight = maximumHeight; + if (self.isShowing) { + [self setNeedsUpdateLayout]; + } +} + +- (UIEdgeInsets)safetyMarginsAvoidSafeAreaInsets { + UIEdgeInsets result = self.safetyMarginsOfSuperview; + if (self.isHorizontalLayoutDirection) { + result.left += self.superview.safeAreaInsets.left; + result.right += self.superview.safeAreaInsets.right; + } else { + result.top += self.superview.safeAreaInsets.top; + result.bottom += self.superview.safeAreaInsets.bottom; + } + return result; +} + @end @implementation QMUIPopupContainerView (UISubclassingHooks) - (void)didInitialize { + _borderLayer = [CAShapeLayer layer]; + [_borderLayer qmui_removeDefaultAnimations]; + _borderLayer.fillColor = UIColor.clearColor.CGColor; + [self.layer addSublayer:_borderLayer]; + _backgroundLayer = [CAShapeLayer layer]; - _backgroundLayer.shadowOffset = CGSizeMake(0, 2); - _backgroundLayer.shadowOpacity = 1; - _backgroundLayer.shadowRadius = 10; + [_backgroundLayer qmui_removeDefaultAnimations]; [self.layer addSublayer:_backgroundLayer]; _contentView = [[UIView alloc] init]; @@ -549,7 +979,7 @@ - (void)didInitialize { [self addSubview:self.contentView]; // 由于浮层是在调用 showWithAnimated: 时才会被添加到 window 上,所以 appearance 也是在 showWithAnimated: 后才生效,这太晚了,会导致 showWithAnimated: 之前用到那些支持 appearance 的属性值都不准确,所以这里手动提前触发。 - [self updateAppearance]; + [self qmui_applyAppearance]; } - (CGSize)sizeThatFitsInContentView:(CGSize)size { @@ -564,19 +994,17 @@ - (CGSize)sizeThatFitsInContentView:(CGSize)size { BOOL isImageViewShowing = [self isSubviewShowing:_imageView]; if (isImageViewShowing) { CGSize imageViewSize = [_imageView sizeThatFits:size]; - resultSize.width += ceil(imageViewSize.width) + self.imageEdgeInsets.left; - resultSize.height += ceil(imageViewSize.height) + self.imageEdgeInsets.top; + resultSize.width += ceil(imageViewSize.width) + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets); + resultSize.height += ceil(imageViewSize.height) + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets); } BOOL isTextLabelShowing = [self isSubviewShowing:_textLabel]; if (isTextLabelShowing) { - CGSize textLabelLimitSize = CGSizeMake(size.width - resultSize.width - self.imageEdgeInsets.right, size.height); + CGSize textLabelLimitSize = CGSizeMake(size.width - resultSize.width - UIEdgeInsetsGetHorizontalValue(self.textEdgeInsets), size.height); CGSize textLabelSize = [_textLabel sizeThatFits:textLabelLimitSize]; - resultSize.width += (isImageViewShowing ? self.imageEdgeInsets.right : 0) + ceil(textLabelSize.width) + self.textEdgeInsets.left; - resultSize.height = MAX(resultSize.height, ceil(textLabelSize.height) + self.textEdgeInsets.top); + resultSize.width += ceil(textLabelSize.width) + UIEdgeInsetsGetHorizontalValue(self.textEdgeInsets); + resultSize.height = MAX(resultSize.height, ceil(textLabelSize.height) + UIEdgeInsetsGetVerticalValue(self.textEdgeInsets)); } - resultSize.width = MIN(size.width, resultSize.width); - resultSize.height = MIN(size.height, resultSize.height); return resultSize; } @@ -600,12 +1028,13 @@ + (void)setDefaultAppearance { appearance.maximumHeight = CGFLOAT_MAX; appearance.minimumHeight = 0; appearance.preferLayoutDirection = QMUIPopupContainerViewLayoutDirectionAbove; + appearance.usesOppositeLayoutAlignmentIfNeeded = YES; appearance.distanceBetweenSource = 5; appearance.safetyMarginsOfSuperview = UIEdgeInsetsMake(10, 10, 10, 10); - appearance.backgroundColor = UIColorWhite; + appearance.backgroundColor = UIColorWhite;// 如果先设置了 UIView.appearance.backgroundColor,再使用最传统的 method_exchangeImplementations 交换 UIView.setBackgroundColor 方法,则会 crash。QMUI 这里是在 +initialize 时设置的,业务如果要 hook -[UIView setBackgroundColor:] 则需要比 +initialize 更早才行 appearance.maskViewBackgroundColor = UIColorMask; appearance.highlightedBackgroundColor = nil; - appearance.shadowColor = UIColorMakeWithRGBA(0, 0, 0, .1); + appearance.shadow = [NSShadow qmui_shadowWithColor:UIColorMakeWithRGBA(0, 0, 0, .1) shadowOffset:CGSizeMake(0, 2) shadowRadius:10]; appearance.borderColor = UIColorGrayLighten; appearance.borderWidth = PixelOne; appearance.cornerRadius = 10; @@ -613,26 +1042,6 @@ + (void)setDefaultAppearance { } -- (void)updateAppearance { - QMUIPopupContainerView *appearance = [QMUIPopupContainerView appearance]; - self.contentEdgeInsets = appearance.contentEdgeInsets; - self.arrowSize = appearance.arrowSize; - self.maximumWidth = appearance.maximumWidth; - self.minimumWidth = appearance.minimumWidth; - self.maximumHeight = appearance.maximumHeight; - self.minimumHeight = appearance.minimumHeight; - self.preferLayoutDirection = appearance.preferLayoutDirection; - self.safetyMarginsOfSuperview = appearance.safetyMarginsOfSuperview; - self.distanceBetweenSource = appearance.distanceBetweenSource; - self.backgroundColor = appearance.backgroundColor; - self.maskViewBackgroundColor = appearance.maskViewBackgroundColor; - self.shadowColor = appearance.shadowColor; - self.borderColor = appearance.borderColor; - self.borderWidth = appearance.borderWidth; - self.cornerRadius = appearance.cornerRadius; - self.qmui_outsideEdge = appearance.qmui_outsideEdge; -} - @end @implementation QMUIPopContainerViewController diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuBaseItem.m b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuBaseItem.m deleted file mode 100644 index 3244a543..00000000 --- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuBaseItem.m +++ /dev/null @@ -1,36 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUIPopupMenuBaseItem.m -// QMUIKit -// -// Created by QMUI Team on 2018/8/21. -// - -#import "QMUIPopupMenuBaseItem.h" - -@implementation QMUIPopupMenuBaseItem - -@synthesize title = _title; -@synthesize height = _height; -@synthesize handler = _handler; -@synthesize menuView = _menuView; - -- (instancetype)init { - if (self = [super init]) { - self.height = -1; - } - return self; -} - -- (void)updateAppearance { - -} - -@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.h deleted file mode 100644 index ee2b6019..00000000 --- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.h +++ /dev/null @@ -1,53 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUIPopupMenuButtonItem.h -// QMUIKit -// -// Created by QMUI Team on 2018/8/21. -// - -#import "QMUIPopupMenuBaseItem.h" - -NS_ASSUME_NONNULL_BEGIN - -@class QMUIButton; - -/** - * 配合 QMUIPopupMenuView 使用,用于可点击的菜单项。 - * 支持显示图片和标题,以及点击事件的回调。 - * 可在 QMUIPopupMenuView 里统一修改菜单项的样式,如果某个菜单项需要特殊调整,可通过 QMUIPopupMenuButtonItem.button 拿到 view 并进行调整。 - */ -@interface QMUIPopupMenuButtonItem : QMUIPopupMenuBaseItem - -/// item 里的图片,默认在左边,也可通过 item.button.imagePosition 修改图片的位置 -@property(nonatomic, strong, nullable) UIImage *image; - -/// 每个 item 都通过一个 button 来显示内容,可直接修改 button 的相关属性达到自定义的效果。 -@property(nonatomic, strong, readonly, nonnull) QMUIButton *button; - -/// item 被点击时的背景色,默认为 TableViewCellSelectedBackgroundColor,与列表的 cell 点击背景色一致。 -@property(nonatomic, strong, nullable) UIColor *highlightedBackgroundColor UI_APPEARANCE_SELECTOR; - -/// item 里图片和文字之间的间距,默认为 6,只有同时存在图片和文字时这个属性才会生效。 -@property(nonatomic, assign) CGFloat imageMarginRight UI_APPEARANCE_SELECTOR; - -/** - 推荐的初始化方法 - - @param image item 的图片 - @param title item 的文字 - @param handler item 点击时的事件回调,需要在这里自行隐藏 aMenuView - @return item - */ -+ (instancetype)itemWithImage:(nullable UIImage *)image title:(nullable NSString *)title handler:(nullable void (^)(QMUIPopupMenuButtonItem *aItem))handler; - -@end - -NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.m b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.m deleted file mode 100644 index f650a00e..00000000 --- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuButtonItem.m +++ /dev/null @@ -1,116 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUIPopupMenuButtonItem.m -// QMUIKit -// -// Created by QMUI Team on 2018/8/21. -// - -#import "QMUIPopupMenuButtonItem.h" -#import "QMUIButton.h" -#import "UIControl+QMUI.h" -#import "QMUIPopupMenuView.h" -#import "QMUICore.h" - -@interface QMUIPopupMenuButtonItem (UIAppearance) - -- (void)updateAppearanceForMenuButtonItem; -@end - -@implementation QMUIPopupMenuButtonItem - -+ (instancetype)itemWithImage:(UIImage *)image title:(NSString *)title handler:(nullable void (^)(QMUIPopupMenuButtonItem *))handler { - QMUIPopupMenuButtonItem *item = [[QMUIPopupMenuButtonItem alloc] init]; - item.image = image; - item.title = title; - item.handler = handler; - return item; -} - -- (instancetype)init { - if (self = [super init]) { - self.height = -1; - - _button = [[QMUIButton alloc] init]; - self.button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; - self.button.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; - [self.button addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self addSubview:self.button]; - - [self updateAppearanceForMenuButtonItem]; - } - return self; -} - -- (CGSize)sizeThatFits:(CGSize)size { - return [self.button sizeThatFits:size]; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - self.button.frame = self.bounds; -} - -- (void)setTitle:(NSString *)title { - [super setTitle:title]; - [self.button setTitle:title forState:UIControlStateNormal]; -} - -- (void)setImage:(UIImage *)image { - _image = image; - [self.button setImage:image forState:UIControlStateNormal]; - if (image) { - self.button.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, self.imageMarginRight); - } else { - self.button.imageEdgeInsets = UIEdgeInsetsZero; - } -} - -- (void)setHighlightedBackgroundColor:(UIColor *)highlightedBackgroundColor { - _highlightedBackgroundColor = highlightedBackgroundColor; - self.button.highlightedBackgroundColor = highlightedBackgroundColor; -} - -- (void)handleButtonEvent:(id)sender { - if (self.handler) { - self.handler(self); - } -} - -- (void)updateAppearance { - self.button.titleLabel.font = self.menuView.itemTitleFont; - [self.button setTitleColor:self.menuView.itemTitleColor forState:UIControlStateNormal]; - self.button.contentEdgeInsets = UIEdgeInsetsMake(0, self.menuView.padding.left, 0, self.menuView.padding.right); -} - -@end - -@implementation QMUIPopupMenuButtonItem (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self setDefaultAppearanceForPopupMenuView]; - }); -} - -+ (void)setDefaultAppearanceForPopupMenuView { - QMUIPopupMenuButtonItem *appearance = [QMUIPopupMenuButtonItem appearance]; - appearance.highlightedBackgroundColor = TableViewCellSelectedBackgroundColor; - appearance.imageMarginRight = 6; -} - -- (void)updateAppearanceForMenuButtonItem { - QMUIPopupMenuButtonItem *appearance = [QMUIPopupMenuButtonItem appearance]; - self.highlightedBackgroundColor = appearance.highlightedBackgroundColor; - self.imageMarginRight = appearance.imageMarginRight; -} - -@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItem.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItem.h new file mode 100644 index 00000000..9e6e443f --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItem.h @@ -0,0 +1,55 @@ +// +// QMUIPopupMenuItem.h +// QMUIKit +// +// Created by molice on 2024/6/17. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import +#import + +@class QMUIPopupMenuView; +@class QMUIPopupMenuItemView; +@protocol QMUIPopupMenuItemViewProtocol; + +NS_ASSUME_NONNULL_BEGIN + +@interface QMUIPopupMenuItem : NSObject + +/// item 里的文字 +@property(nonatomic, copy, nullable) NSString *title; + +/// item 里的第二行文字 +@property(nonatomic, copy, nullable) NSString *subtitle; + +/// item 里的图片,默认会以 template 形式渲染,也即由 tintColor 决定颜色,可显式声明为 AlwaysOriginal 来以图片原本的颜色显示。 +@property(nonatomic, strong, nullable) UIImage *image; + +/// item 的高度,默认为 -1,-1 表示高度以 QMUIPopupMenuView.itemHeight 为准。如果设置为 QMUIViewSelfSizingHeight,则表示高度由 -[self sizeThatFits:] 返回的值决定。 +@property(nonatomic, assign) CGFloat height; + +/// 每次将 item 关联到 itemView 上时都会调用这个 block,可以理解为在 @c QMUIPopupMenuView.itemViewConfigurationHandler 之后立马会调用 @c QMUIPopupMenuItem.configurationBlock 。 +/// 业务可利用这个 block 做一些自定义的配置 itemView 的行为。 +@property(nonatomic, copy) void (^configurationBlock)(__kindof QMUIPopupMenuItem *aItem, __kindof UIControl *aItemView, NSInteger section, NSInteger index); + +/// item 被点击时的事件处理接口 +/// @note 需要在内部自行隐藏 QMUIPopupMenuView。 +@property(nonatomic, copy, nullable) void (^handler)(__kindof QMUIPopupMenuItem *aItem, __kindof UIControl *aItemView, NSInteger section, NSInteger index); + +/// 当前 item 所在的 QMUIPopupMenuView 的引用,只有在 item 被添加到菜单之后才有值。 +@property(nonatomic, weak, nullable) __kindof QMUIPopupMenuView *menuView; + ++ (instancetype)itemWithTitle:(nullable NSString *)title + handler:(void (^ __nullable)(__kindof QMUIPopupMenuItem *aItem, __kindof UIControl *aItemView, NSInteger section, NSInteger index))handler; ++ (instancetype)itemWithImage:(nullable UIImage *)image + title:(nullable NSString *)title + handler:(void (^ __nullable)(__kindof QMUIPopupMenuItem *aItem, __kindof UIControl *aItemView, NSInteger section, NSInteger index))handler; ++ (instancetype)itemWithImage:(nullable UIImage *)image + title:(nullable NSString *)title + subtitle:(nullable NSString *)subtitle + handler:(void (^ __nullable)(__kindof QMUIPopupMenuItem *aItem, __kindof UIControl *aItemView, NSInteger section, NSInteger index))handler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItem.m b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItem.m new file mode 100644 index 00000000..134def9d --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItem.m @@ -0,0 +1,38 @@ +// +// QMUIPopupMenuItem.m +// QMUIKit +// +// Created by molice on 2024/6/17. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import "QMUIPopupMenuItem.h" + +@implementation QMUIPopupMenuItem + +- (instancetype)init { + self = [super init]; + if (self) { + _height = -1; + } + return self; +} + ++ (instancetype)itemWithImage:(UIImage *)image title:(NSString *)title subtitle:(NSString *)subtitle handler:(void (^ _Nullable)(__kindof QMUIPopupMenuItem * _Nonnull, __kindof UIControl * _Nonnull, NSInteger, NSInteger))handler { + QMUIPopupMenuItem *item = [[self alloc] init]; + item.image = image; + item.title = title; + item.subtitle = subtitle; + item.handler = handler; + return item; +} + ++ (instancetype)itemWithTitle:(NSString *)title handler:(void (^ _Nullable)(__kindof QMUIPopupMenuItem * _Nonnull, __kindof UIControl * _Nonnull, NSInteger, NSInteger))handler { + return [self itemWithImage:nil title:title subtitle:nil handler:handler]; +} + ++ (instancetype)itemWithImage:(UIImage *)image title:(NSString *)title handler:(void (^ _Nullable)(__kindof QMUIPopupMenuItem * _Nonnull, __kindof UIControl * _Nonnull, NSInteger, NSInteger))handler { + return [self itemWithImage:image title:title subtitle:nil handler:handler]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemProtocol.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemProtocol.h deleted file mode 100644 index 1e464a5f..00000000 --- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemProtocol.h +++ /dev/null @@ -1,41 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUIPopupMenuItemProtocol.h -// QMUIKit -// -// Created by QMUI Team on 2018/8/21. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@class QMUIPopupMenuView; - -@protocol QMUIPopupMenuItemProtocol - -/// item 里的文字 -@property(nonatomic, copy, nullable) NSString *title; - -/// item 的高度,默认为 -1。小于 0 的值均表示高度以 QMUIPopupMenuView.itemHeight 为准,当需要给某个 item 指定特定高度时才需要用这个 height 属性。 -@property(nonatomic, assign) CGFloat height; - -/// item 被点击时的事件处理,需要在内部自行隐藏 QMUIPopupMenuView。 -@property(nonatomic, copy, nullable) void (^handler)(__kindof NSObject *aItem); - -/// 当前 item 所在的 QMUIPopupMenuView 的引用,只有在 item 被添加到菜单之后才有值。 -@property(nonatomic, weak, nullable) QMUIPopupMenuView *menuView; - -/// item 被添加到 menuView 之后(也即 menuView 属性有值了)会被调用,可在这个方法里更新 item 的样式(因为某些样式可能需要从 menuView 那边读取) -- (void)updateAppearance; - -@end - -NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemView.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemView.h new file mode 100644 index 00000000..ecf6da90 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemView.h @@ -0,0 +1,34 @@ +// +// QMUIPopupMenuItemView.h +// QMUIKit +// +// Created by molice on 2024/6/17. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import +#import "QMUIPopupMenuItemViewProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIButton; +@class QMUICheckbox; + +@interface QMUIPopupMenuItemView : UIControl + +/// 图片、文本、第二行文本所在的 view,不接受事件,点击事件由 self 接管。 +@property(nonatomic, strong, readonly) QMUIButton *button; + +/// 当菜单进入选择模式时,代表被选中的勾。非选择模式时不存在。 +@property(nonatomic, strong, readonly, nullable) UIImageView *checkmark; + +/// 当菜单进入选择模式时,代表被选中的圆形勾,不接受事件,勾选状态由菜单控制。非选择模式时不存在。 +@property(nonatomic, strong, readonly, nullable) QMUICheckbox *checkbox; + +@property(nonatomic, strong, nullable) UIColor *highlightedBackgroundColor; + +@property(nonatomic, assign) UIEdgeInsets padding; +@property(nonatomic, assign) CGFloat spacingBetweenButtonAndCheck; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemView.m b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemView.m new file mode 100644 index 00000000..878d9b43 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemView.m @@ -0,0 +1,186 @@ +// +// QMUIPopupMenuItemView.m +// QMUIKit +// +// Created by molice on 2024/6/17. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import "QMUIPopupMenuItemView.h" +#import "QMUICore.h" +#import "UIControl+QMUI.h" +#import "QMUIPopupMenuView.h" +#import "QMUILayouter.h" +#import "QMUIButton.h" +#import "QMUICheckbox.h" +#import "UIView+QMUI.h" + +@interface QMUIPopupMenuItemView () +@property(nonatomic, assign) QMUIPopupMenuSelectedStyle selectedStyle; +@property(nonatomic, assign) QMUIPopupMenuSelectedLayout selectedLayout; +@end + +@implementation QMUIPopupMenuItemView + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + _button = [[QMUIButton alloc] init]; + _button.userInteractionEnabled = NO; + _button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeading; + _button.spacingBetweenImageAndTitle = 12; + _button.titleLabel.font = UIFontMake(16); + _button.subtitleLabel.font = UIFontMake(14); + _button.subtitleLabel.alpha = .6; + _button.adjustsTitleTintColorAutomatically = YES; + _button.tintColor = nil;// 跟随 superview + [self addSubview:_button]; + + _padding = UIEdgeInsetsMake(8, 0, 8, 0); + _spacingBetweenButtonAndCheck = 16; + + if (QMUICMIActivated) { + self.highlightedBackgroundColor = TableViewGroupedCellSelectedBackgroundColor; + } + } + return self; +} + +- (QMUILayouterItem *)generateLayouter { + QMUILayouterItem *button = [QMUILayouterItem itemWithView:self.button margin:UIEdgeInsetsZero grow:1 shrink:QMUILayouterShrinkDefault]; + UIView *checkView = self.selectedStyle == QMUIPopupMenuSelectedStyleCheckmark ? self.checkmark : (self.selectedStyle == QMUIPopupMenuSelectedStyleCheckbox ? self.checkbox : nil); + QMUILayouterItem *check = checkView ? [QMUILayouterItem itemWithView:checkView margin:UIEdgeInsetsZero grow:QMUILayouterGrowNever shrink:QMUILayouterShrinkNever] : nil; + check.visibleBlock = ^BOOL(QMUILayouterItem * _Nonnull aItem) { + return YES;// 不管 checkView 显示与否都一定占位,避免切换 selected 过程中内容宽度跳动 + }; + NSArray *items = nil; + if (check) { + if (self.selectedLayout == QMUIPopupMenuSelectedLayoutAtEnd) { + items = @[button, check]; + } else if (self.selectedLayout == QMUIPopupMenuSelectedLayoutAtStart) { + items = @[check, button]; + } + } else { + items = @[button]; + } + QMUILayouterLinearHorizontal *h = [QMUILayouterLinearHorizontal itemWithChildItems:items spacingBetweenItems:_spacingBetweenButtonAndCheck horizontal:QMUILayouterAlignmentFill vertical:QMUILayouterAlignmentCenter]; + h.margin = self.padding; + QMUILayouterLinearHorizontal *container = [QMUILayouterLinearHorizontal itemWithChildItems:@[h] spacingBetweenItems:0 horizontal:QMUILayouterAlignmentFill vertical:QMUILayouterAlignmentCenter]; + return container; +} + +- (CGSize)sizeThatFits:(CGSize)size { + return [[self generateLayouter] sizeThatFits:size]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + QMUILayouterItem *l = [self generateLayouter]; + l.frame = self.bounds; + [l layoutIfNeeded]; +} + +- (void)setEnabled:(BOOL)enabled { + [super setEnabled:enabled]; + [self updateAlphaState]; +} + +- (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; + [self updateAlphaState]; +} + +- (void)setSelected:(BOOL)selected { + [super setSelected:selected]; + self.button.selected = selected;// 同步状态以使 button 上也可以感知到 selected + if (self.selectedStyle == QMUIPopupMenuSelectedStyleCheckmark) { + self.checkmark.hidden = !selected; + } else if (self.selectedStyle == QMUIPopupMenuSelectedStyleCheckbox) { + self.checkbox.hidden = NO; + self.checkbox.selected = selected; + } else { + self.checkmark.hidden = YES; + self.checkbox.hidden = YES; + self.checkbox.selected = NO; + } +} + +- (void)setSelectedStyle:(QMUIPopupMenuSelectedStyle)selectedStyle { + _selectedStyle = selectedStyle; + if (selectedStyle == QMUIPopupMenuSelectedStyleCheckmark) { + if (!_checkmark) { + _checkmark = [[UIImageView alloc] initWithImage:[TableViewCellCheckmarkImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]]; + [self addSubview:_checkmark]; + } + _checkmark.hidden = !self.selected; + _checkbox.hidden = YES; + } else if (selectedStyle == QMUIPopupMenuSelectedStyleCheckbox) { + if (!_checkbox) { + _checkbox = QMUICheckbox.new; + _checkbox.tintColor = nil; + _checkbox.userInteractionEnabled = NO; + [self addSubview:_checkbox]; + } + _checkbox.hidden = NO; + _checkbox.selected = self.selected; + _checkmark.hidden = YES; + } else { + _checkmark.hidden = YES; + _checkbox.hidden = YES; + } + [self setNeedsLayout]; +} + +- (void)setSelectedLayout:(QMUIPopupMenuSelectedLayout)selectedLayout { + _selectedLayout = selectedLayout; + [self setNeedsLayout]; +} + +- (void)updateAlphaState { + if (!self.enabled) { + [self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + obj.alpha = UIControlDisabledAlpha; + }]; + if (self.highlightedBackgroundColor) { + self.backgroundColor = nil; + } + return; + } + + [self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + obj.alpha = 1; + }]; + if (self.highlighted) { + if (self.highlightedBackgroundColor) { + self.backgroundColor = self.highlightedBackgroundColor; + } + return; + } + if (self.highlightedBackgroundColor) { + self.backgroundColor = nil; + } +} + +#pragma mark - + +@synthesize item = _item; +- (void)setItem:(__kindof QMUIPopupMenuItem *)item { + _item = item; + [self.button setImage:item.image.renderingMode == UIImageRenderingModeAutomatic ? [item.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] : item.image forState:UIControlStateNormal]; + [self.button setTitle:item.title forState:UIControlStateNormal]; + self.button.subtitle = item.subtitle; + + QMUIPopupMenuView *menu = item.menuView; + + self.padding = UIEdgeInsetsMake(self.padding.top, menu.padding.left, self.padding.bottom, menu.padding.right); + + if (menu.allowsSelection) { + self.selectedStyle = menu.selectedStyle; + self.selectedLayout = menu.selectedLayout; + } else { + self.selectedStyle = (QMUIPopupMenuSelectedStyle)-1;// 表示清空 + } + + [self setNeedsLayout]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemViewProtocol.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemViewProtocol.h new file mode 100644 index 00000000..18686536 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemViewProtocol.h @@ -0,0 +1,23 @@ +// +// QMUIPopupMenuItemViewProtocol.h +// QMUIKit +// +// Created by molice on 2024/6/17. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIPopupMenuItem; + +@protocol QMUIPopupMenuItemViewProtocol + +@required + +/// 当前 itemView 关联的 item,在 cellForRow 时会被设置。itemView 内所有与 item 强相关的内容均应在 setItem: 方法里设置。 +@property(nonatomic, weak, nullable) __kindof QMUIPopupMenuItem *item; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h index 9dd7785e..94538d7d 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIPopupMenuView.h @@ -15,26 +15,47 @@ #import #import "QMUIPopupContainerView.h" -#import "QMUIPopupMenuItemProtocol.h" -#import "QMUIPopupMenuBaseItem.h" -#import "QMUIPopupMenuButtonItem.h" +#import "QMUIPopupMenuItemViewProtocol.h" +#import "QMUIPopupMenuItem.h" +#import "QMUITableView.h" +#import "QMUILabel.h" +#import "QMUIPopupMenuItemView.h" NS_ASSUME_NONNULL_BEGIN +typedef NS_ENUM(NSInteger, QMUIPopupMenuSelectedStyle) { + QMUIPopupMenuSelectedStyleCheckmark, // 小勾 + QMUIPopupMenuSelectedStyleCheckbox, // 圆形勾 + QMUIPopupMenuSelectedStyleCustom, // 自定义,默认不做任何表现,交给业务自行处理 +}; + +typedef NS_ENUM(NSInteger, QMUIPopupMenuSelectedLayout) { + QMUIPopupMenuSelectedLayoutAtEnd, + QMUIPopupMenuSelectedLayoutAtStart, +}; + /** * 用于弹出浮层里显示一行一行的菜单的控件。 * 使用方式: * 1. 调用 init 方法初始化。 * 2. 按需设置分隔线、item 高度等样式。 - * 3. 设置完样式后再通过 items 或 itemSections 添加菜单项。 + * 3. 设置完样式后再通过 items 或 itemSections 添加菜单项,并在 item 点击事件里调用 hideWithAnimated: 隐藏浮层。 * 4. 通过为 sourceBarItem/sourceView/sourceRect 三者中的一个赋值,来决定浮层布局的位置(参考父类)。 * 5. 调用 showWithAnimated: 即可显示(参考父类)。 * - * 注意,QMUIPopupMenuView 的大小默认是按内容自适应的(item 的 sizeThatFits),但同时又受 maximumWidth/minimumWidth 的限制。 + * 注意,QMUIPopupMenuView 的大小默认是按内容自适应的(item 的 sizeThatFits),但同时又受 adjustsWidthAutomatically/maximumWidth/minimumWidth 的控制。 + * + * 关于颜色的设置: + * 1. 如果整个菜单的颜色(包括图片、title、subtitle、checkmark、checkbox)均一致,则直接通过 menu.tintColor 设置即可,默认情况下这些元素的 tintColor 都是 nil,也即跟随 superview 的 tintColor 走。 + * 2. 如果 item 里某个元素的颜色与整体相比有差异化的诉求,则需要继承 QMUIPopupMenuItemView 实现一个子类,在子类的 setHighlighted:、setSelected:、tintColorDidChange 里处理,然后通过 menu.itemViewGenerator 返回这个子类。 + * 3. 特别的,QMUIPopupMenuItem.image 默认会以 AlwaysTemplate 方式渲染,也即由 tintColor 决定图片颜色,可显式声明为 AlwaysOriginal 来保持图片原始的颜色。 */ -@interface QMUIPopupMenuView : QMUIPopupContainerView +@interface QMUIPopupMenuView : QMUIPopupContainerView + +/// contentView 里的 scrollView,所有 itemButton 都是放在这里面的。 +@property(nonatomic, strong, readonly) QMUITableView *tableView; -/// 是否需要显示每个 item 之间的分隔线,默认为 NO,当为 YES 时,每个 section 除了最后一个 item 外其他 item 底部都会显示分隔线。 +/// 是否需要显示每个 item 之间的分隔线,默认为 NO,当为 YES 时,每个 section 除了最后一个 item 外其他 item 底部都会显示分隔线。分隔线显示在当前 item 上方,不占位。 @property(nonatomic, assign) BOOL shouldShowItemSeparator UI_APPEARANCE_SELECTOR; /// item 分隔线的颜色,默认为 UIColorSeparator。 @@ -43,35 +64,98 @@ NS_ASSUME_NONNULL_BEGIN /// item 分隔线的位置偏移,默认为 UIEdgeInsetsZero。item 分隔线的默认布局是 menuView 宽度减去左右 padding,如果你希望分隔线左右贴边则可为这个属性设置一个负值的 left/right。 @property(nonatomic, assign) UIEdgeInsets itemSeparatorInset UI_APPEARANCE_SELECTOR; -/// 是否显示 section 和 section 之间的分隔线,默认为 NO,当为 YES 时,除了最后一个 section,其他 section 底部都会显示一条分隔线。 +/// item 分隔线的高度,默认为 PixelOne。分隔线拥有自己的占位,不与 item 重叠。 +@property(nonatomic, assign) CGFloat itemSeparatorHeight UI_APPEARANCE_SELECTOR; + +/// 是否显示 section 和 section 之间的分隔线,默认为 NO,当为 YES 时,除了最后一个 section,其他 section 底部都会显示一条分隔线。分隔线拥有自己的占位,不与 item、sectionSpacing 重叠。 @property(nonatomic, assign) BOOL shouldShowSectionSeparator UI_APPEARANCE_SELECTOR; -/// section 分隔线的颜色,默认为 UIColorSeparator。 +/// section 分隔线的颜色,默认为 UIColorSeparator。分隔线拥有自己的占位,不与 sectionSpacing 重叠。 @property(nonatomic, strong, nullable) UIColor *sectionSeparatorColor UI_APPEARANCE_SELECTOR; /// section 分隔线的位置偏移,默认为 UIEdgeInsetsZero。section 分隔线的默认布局是撑满整个 menuView,如果你不希望分隔线左右贴边则可为这个属性设置一个 left/right 不为 0 的值即可。 @property(nonatomic, assign) UIEdgeInsets sectionSeparatorInset UI_APPEARANCE_SELECTOR; -/// item 里文字的字体,默认为 UIFontMake(16)。 -@property(nonatomic, strong, nullable) UIFont *itemTitleFont UI_APPEARANCE_SELECTOR; +/// section 分隔线的高度,默认为 PixelOne。 +@property(nonatomic, assign) CGFloat sectionSeparatorHeight UI_APPEARANCE_SELECTOR; + +/// section 之间的间隔,默认为0,也即贴合到一起。 +@property(nonatomic, assign) CGFloat sectionSpacing UI_APPEARANCE_SELECTOR; + +/// section 之间的间隔颜色,当 sectionSpacing > 0 时才有意义,默认为 UIColorSeparator。 +@property(nonatomic, strong, nullable) UIColor *sectionSpacingColor UI_APPEARANCE_SELECTOR; -/// item 里文字的颜色,默认为 UIColorBlue -@property(nonatomic, strong, nullable) UIColor *itemTitleColor UI_APPEARANCE_SELECTOR; +/// 批量设置 sectionTitleLabel 的样式 +@property(nonatomic, copy, nullable) void (^sectionTitleConfigurationHandler)(__kindof QMUIPopupMenuView *aMenuView, QMUILabel *sectionTitleLabel, NSInteger section); /// 整个 menuView 内部上下左右的 padding,其中 padding.left/right 会被作为 item.button.contentEdgeInsets.left/right,也即每个 item 的宽度一定是撑满整个 menuView 的。 @property(nonatomic, assign) UIEdgeInsets padding UI_APPEARANCE_SELECTOR; /// 每个 item 的统一高度,默认为 44。如果某个 item 设置了自己的 height,则不受 itemHeight 属性的约束。 +/// 如果将 itemHeight 设置为 QMUIViewSelfSizingHeight 则会以 item sizeThatFits: 返回的结果作为最终的 item 高度。 @property(nonatomic, assign) CGFloat itemHeight UI_APPEARANCE_SELECTOR; -/// 批量设置 item 的样式 -@property(nonatomic, copy, nullable) void (^itemConfigurationHandler)(QMUIPopupMenuView *aMenuView, __kindof QMUIPopupMenuBaseItem *aItem, NSInteger section, NSInteger index); +/// 默认 YES,也即会自动计算每个 item 的宽度,取其中最宽的值作为整个 menu 的宽度。 +/// 当数据量大的情况下请手动置为 NO 并改为用 maximumWidth、minimumWidth 控制 menu 宽度,从而获取更优的性能。 +@property(nonatomic, assign) BOOL adjustsWidthAutomatically; + +/// item、sectionTitle 之间是否复用以提升性能,默认为 NO。 +/// 当数据量大或有复杂异步场景的情况下可改为 YES。 +/// 若需要修改值,建议在设置 items/sectionItems 之前就先设置好。 +@property(nonatomic, assign) BOOL shouldReuseItems; + +/// 当需要创建一个 itemView 时会试图从这个 block 获取,若业务没实现这个 block,则默认返回一个 @c QMUIPopupMenuItemView 实例。 +@property(nonatomic, copy, nullable) __kindof UIControl * (^itemViewGenerator)(__kindof QMUIPopupMenuView *aMenuView); + +/// 批量设置 itemView 的样式 +@property(nonatomic, copy, nullable) void (^itemViewConfigurationHandler)(__kindof QMUIPopupMenuView *aMenuView, __kindof QMUIPopupMenuItem *aItem, __kindof UIControl *aItemView, NSInteger section, NSInteger index); /// 设置 item,均处于同一个 section 内 -@property(nonatomic, copy, nullable) NSArray<__kindof QMUIPopupMenuBaseItem *> *items; +@property(nonatomic, copy, nullable) NSArray<__kindof QMUIPopupMenuItem *> *items; /// 设置多个 section 的多个 item -@property(nonatomic, copy, nullable) NSArray *> *itemSections; +@property(nonatomic, copy, nullable) NSArray *> *itemSections; + +/// 为每个 section 设置标题,不需要显示标题的 section 请使用空字符串占位。必须保证 @c sectionTitles 和 @c itemSections 长度相等。 +/// @note 请在设置 item、itemSections 之前先设置本属性。 +@property(nonatomic, copy, nullable) NSArray *sectionTitles; + +/// 是否允许出现勾选,默认为 NO。 +@property(nonatomic, assign) BOOL allowsSelection; + +/// 是否允许多选,默认为 NO。当置为 YES 时会同时把 @c allowsSelection 也置为 YES。所以如果你只是想判断当前是否处于勾选状态,不关心单选还是多选,则直接访问 @c allowsSelection 即可。 +@property(nonatomic, assign) BOOL allowsMultipleSelection; + +/// 勾选的样式,默认为 checkmark。 +@property(nonatomic, assign) QMUIPopupMenuSelectedStyle selectedStyle; + +/// 勾选出现的位置,默认为 AtEnd,也即在按钮右侧。 +@property(nonatomic, assign) QMUIPopupMenuSelectedLayout selectedLayout; + +/// 当前选中的 item 序号,若当前是多选,则会返回第一个被选中的 item 的序号。 +/// 若想清空选中状态,可赋值为 @c NSNotFound ,默认为 @c NSNotFound 。 +/// @warning 仅用于单 section 的场景,多 section 场景请使用 @c selectedItemIndexPath 。 +@property(nonatomic, assign) NSInteger selectedItemIndex; + +/// 当前选中的 item 序号,若当前是多选,则会返回第一个被选中的 item 的序号。 +/// 若想清空选中状态,可赋值为 @c nil ,默认为 @c nil 。 +/// @note 可用于多 section 的场景。 +@property(nonatomic, strong, nullable) NSIndexPath *selectedItemIndexPath; + +/// 当前选中的所有 item 的序号。 +/// 若想清空选中状态,可赋值为 @c nil ,默认为 @c nil 。 +@property(nonatomic, strong, nullable) NSArray *selectedItemIndexPaths; + +/// 当处于 @c allowsSelection 模式时,默认每个 item 都可被选中。如果希望某个 item 不参与 selected 操作,可通过该 block 返回 NO 来实现。 +/// 如果想实现“最少选择n个”或“选择任意一个后无法再清空选择”的交互,也可通过这个 block 实现。 +@property(nonatomic, copy, nullable) BOOL (^shouldSelectItemBlock)(__kindof QMUIPopupMenuItem *aItem, __kindof UIControl *aItemView, NSInteger section, NSInteger index); + +/// 固定显示在菜单底部的 view,不跟随滚动,大小通过调用自身的 sizeThatFits: 获取。 +/// @note 菜单的 padding 会作用在 item 上(也即列表),不会作用在 bottomAccessoryView 上,bottomAccessoryView 始终都是宽度撑满菜单,底部紧贴菜单。 +@property(nonatomic, strong, nullable) __kindof UIView *bottomAccessoryView; + +/// 刷新当前菜单的内容及布局 +- (void)reload; @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.m b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.m index e3de90c4..9e158517 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIPopupMenuView.m @@ -18,12 +18,92 @@ #import "UIView+QMUI.h" #import "CALayer+QMUI.h" #import "NSArray+QMUI.h" +#import "UIFont+QMUI.h" +#import "UITableViewCell+QMUI.h" -@interface QMUIPopupMenuView () +@interface QMUIPopupMenuCell : UITableViewCell +@property(nonatomic, strong) __kindof UIControl *itemView; +@end + +@implementation QMUIPopupMenuCell + +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { + if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { + self.selectionStyle = UITableViewCellSelectionStyleNone; + self.backgroundColor = UIColor.clearColor; + } + return self; +} + +- (void)setItemView:(__kindof UIControl *)itemView { + if (_itemView) return; + _itemView = itemView; + [self.contentView addSubview:itemView]; +} + +- (CGSize)sizeThatFits:(CGSize)size { + CGSize result = [self.itemView sizeThatFits:size]; + result.height += self.qmui_borderWidth; + return result; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + self.itemView.frame = CGRectInsetEdges(self.contentView.bounds, UIEdgeInsetsMake(0, 0, self.qmui_borderWidth, 0)); +} + +@end + +@interface QMUIPopupMenuSectionHeaderView : UITableViewHeaderFooterView +@property(nonatomic, strong) QMUILabel *label; +@end + +@implementation QMUIPopupMenuSectionHeaderView + +- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier { + if (self = [super initWithReuseIdentifier:reuseIdentifier]) { + _label = QMUILabel.new; + _label.numberOfLines = 0; + _label.font = UIFontMediumMake(13); + _label.textColor = UIColorGray; + _label.contentEdgeInsets = UIEdgeInsetsMake(12, 16, 2, 16); + [self.contentView addSubview:self.label]; + } + return self; +} + +- (CGSize)sizeThatFits:(CGSize)size { + return [self.label sizeThatFits:size]; +} -@property(nonatomic, strong) UIScrollView *scrollView; -@property(nonatomic, strong) NSMutableArray *itemSeparatorLayers; -@property(nonatomic, strong) NSMutableArray *sectionSeparatorLayers; +- (void)layoutSubviews { + [super layoutSubviews]; + self.label.frame = self.contentView.bounds; +} + +@end + +@interface QMUIPopupMenuSectionFooterView : UITableViewHeaderFooterView +@end + +@implementation QMUIPopupMenuSectionFooterView + +- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier { + if (self = [super initWithReuseIdentifier:reuseIdentifier]) { + self.backgroundView = [[UIView alloc] init];// 去掉默认的背景,以便屏蔽系统对背景色的控制 + } + return self; +} + +// 系统的 UITableViewHeaderFooterView 不允许修改 backgroundColor,都应该放到 backgroundView 里,但却没有在文档中写明,只有不小心误用时才会在 Xcode 控制台里提示,所以这里做个转换,保护误用的情况。 +- (void)setBackgroundColor:(UIColor *)backgroundColor { +// [super setBackgroundColor:backgroundColor]; + self.backgroundView.backgroundColor = backgroundColor; +} + +@end + +@interface QMUIPopupMenuView () @end @interface QMUIPopupMenuView (UIAppearance) @@ -33,185 +113,453 @@ - (void)updateAppearanceForPopupMenuView; @implementation QMUIPopupMenuView -- (void)setItems:(NSArray<__kindof QMUIPopupMenuBaseItem *> *)items { - [_items enumerateObjectsUsingBlock:^(__kindof QMUIPopupMenuBaseItem * _Nonnull item, NSUInteger idx, BOOL * _Nonnull stop) { - item.menuView = nil; - }]; +- (void)setItems:(NSArray<__kindof QMUIPopupMenuItem *> *)items { _items = items; - if (!items) { - self.itemSections = nil; - } else { - self.itemSections = @[_items]; - } + self.itemSections = items ? @[_items] : nil; } -- (void)setItemSections:(NSArray *> *)itemSections { - [_itemSections qmui_enumerateNestedArrayWithBlock:^(__kindof QMUIPopupMenuBaseItem * item, BOOL *stop) { +- (void)setItemSections:(NSArray *> *)itemSections { + [_itemSections qmui_enumerateNestedArrayWithBlock:^(__kindof QMUIPopupMenuItem *item, BOOL *stop) { item.menuView = nil; }]; _itemSections = itemSections; - [self configureItems]; + [_itemSections qmui_enumerateNestedArrayWithBlock:^(__kindof QMUIPopupMenuItem *item, BOOL * _Nonnull stop) { + item.menuView = self; + }]; + [self reload];// 涉及到数据的必须立即刷新,否则容易因为异步导致 cell 里的 view 和当前的 item 不匹配的 bug } -- (void)setItemConfigurationHandler:(void (^)(QMUIPopupMenuView *, __kindof QMUIPopupMenuBaseItem *, NSInteger, NSInteger))itemConfigurationHandler { - _itemConfigurationHandler = [itemConfigurationHandler copy]; - if (_itemConfigurationHandler && self.itemSections.count) { - for (NSInteger section = 0, sectionCount = self.itemSections.count; section < sectionCount; section ++) { - NSArray *items = self.itemSections[section]; - for (NSInteger row = 0, rowCount = items.count; row < rowCount; row ++) { - QMUIPopupMenuBaseItem *item = items[row]; - _itemConfigurationHandler(self, item, section, row); - } +- (void)setSectionTitles:(NSArray *)sectionTitles { + _sectionTitles = sectionTitles; + [self reload]; +} + +- (void)setItemViewConfigurationHandler:(void (^)(__kindof QMUIPopupMenuView * _Nonnull, __kindof QMUIPopupMenuItem * _Nonnull, __kindof UIControl * _Nonnull, NSInteger, NSInteger))itemViewConfigurationHandler { + _itemViewConfigurationHandler = [itemViewConfigurationHandler copy]; + [self setNeedsReload]; +} + +- (void)setSectionTitleConfigurationHandler:(void (^)(__kindof QMUIPopupMenuView * _Nonnull, QMUILabel * _Nonnull, NSInteger))sectionTitleConfigurationHandler { + _sectionTitleConfigurationHandler = [sectionTitleConfigurationHandler copy]; + [self setNeedsReload]; +} + +- (void)setPadding:(UIEdgeInsets)padding { + _padding = padding; + self.tableView.contentInset = UIEdgeInsetsMake(padding.top, self.tableView.contentInset.left, padding.bottom, self.tableView.contentInset.right); + [self setNeedsReload]; +} + +- (void)setShouldShowItemSeparator:(BOOL)shouldShowItemSeparator { + _shouldShowItemSeparator = shouldShowItemSeparator; + [self setNeedsReload]; +} + +- (void)setItemSeparatorInset:(UIEdgeInsets)itemSeparatorInset { + _itemSeparatorInset = itemSeparatorInset; + [self setNeedsReload]; +} + +- (void)setShouldShowSectionSeparator:(BOOL)shouldShowSectionSeparator { + _shouldShowSectionSeparator = shouldShowSectionSeparator; + [self setNeedsReload]; +} + +- (void)setSectionSeparatorHeight:(CGFloat)sectionSeparatorHeight { + _sectionSeparatorHeight = sectionSeparatorHeight; + [self setNeedsReload]; +} + +- (void)setItemHeight:(CGFloat)itemHeight { + _itemHeight = itemHeight; + [self setNeedsReload]; +} + +- (void)setSelectedStyle:(QMUIPopupMenuSelectedStyle)selectedStyle { + _selectedStyle = selectedStyle; + [self setNeedsReload]; +} + +- (void)setSelectedLayout:(QMUIPopupMenuSelectedLayout)selectedLayout { + _selectedLayout = selectedLayout; + [self setNeedsReload]; +} + +- (void)setAllowsSelection:(BOOL)allowsSelection { + _allowsSelection = allowsSelection; + if (!allowsSelection) { + self.selectedItemIndexPaths = nil; + } + [self setNeedsReload]; +} + +- (void)setAllowsMultipleSelection:(BOOL)allowsMultipleSelection { + _allowsMultipleSelection = allowsMultipleSelection; + if (allowsMultipleSelection) { + _allowsSelection = YES; + } else { + if (self.selectedItemIndexPaths.count > 1) { + self.selectedItemIndexPaths = [self.selectedItemIndexPaths subarrayWithRange:NSMakeRange(0, 1)]; } } + [self setNeedsReload]; } -- (void)configureItems { - NSInteger globalItemIndex = 0; - NSInteger separatorIndex = 0; - - // 移除所有 item - [self.scrollView qmui_removeAllSubviews]; - [self.itemSeparatorLayers enumerateObjectsUsingBlock:^(CALayer * _Nonnull layer, NSUInteger idx, BOOL * _Nonnull stop) { - layer.hidden = YES; - }]; - [self.sectionSeparatorLayers enumerateObjectsUsingBlock:^(CALayer * _Nonnull layer, NSUInteger idx, BOOL * _Nonnull stop) { - layer.hidden = YES; - }]; - +BeginIgnoreClangWarning(-Wunused-property-ivar) +- (void)setSelectedItemIndex:(NSInteger)selectedItemIndex { + if (selectedItemIndex == NSNotFound) { + self.selectedItemIndexPath = nil; + } else { + self.selectedItemIndexPath = [NSIndexPath indexPathForRow:selectedItemIndex inSection:0]; + } +} + +- (void)setSelectedItemIndexPath:(NSIndexPath *)selectedItemIndexPath { + self.selectedItemIndexPaths = selectedItemIndexPath ? @[selectedItemIndexPath] : nil; +} +EndIgnoreClangWarning + +- (void)setSelectedItemIndexPaths:(NSArray *)selectedItemIndexPaths { + if (!selectedItemIndexPaths.count) { + _selectedItemIndex = NSNotFound; + _selectedItemIndexPath = nil; + } else { + _selectedItemIndex = selectedItemIndexPaths.firstObject.row; + _selectedItemIndexPath = selectedItemIndexPaths.firstObject; + } + _selectedItemIndexPaths = selectedItemIndexPaths; + [self setNeedsReload]; +} + +- (void)setNeedsReload { + if (_shouldInvalidateLayout) return; + _shouldInvalidateLayout = YES; + dispatch_async(dispatch_get_main_queue(), ^{ + if (self->_shouldInvalidateLayout) { + [self reload]; + } + }); +} + +- (void)reload { + [self.tableView reloadData]; + if (self.isShowing) { + [self updateLayout];// updateLayout 的 super 实现里会把 _shouldInvalidateLayout 置为 NO + } +} + +- (void)updateLayout { + [self setNeedsLayout]; + [self layoutIfNeeded]; + [super updateLayout]; +} + +- (NSIndexPath *)indexPathForItem:(__kindof QMUIPopupMenuItem *)aItem { for (NSInteger section = 0, sectionCount = self.itemSections.count; section < sectionCount; section ++) { - NSArray *items = self.itemSections[section]; + NSArray<__kindof QMUIPopupMenuItem *> *items = self.itemSections[section]; for (NSInteger row = 0, rowCount = items.count; row < rowCount; row ++) { - QMUIPopupMenuBaseItem *item = items[row]; - item.menuView = self; - [item updateAppearance]; - if (self.itemConfigurationHandler) { - self.itemConfigurationHandler(self, item, section, row); + QMUIPopupMenuItem *item = items[row]; + if (item == aItem) { + return [NSIndexPath indexPathForRow:row inSection:section]; } - [self.scrollView addSubview:item]; - - // 配置分隔线,注意每一个 section 里的最后一行是不显示分隔线的 - BOOL shouldShowItemSeparator = self.shouldShowItemSeparator && row < rowCount - 1; - if (shouldShowItemSeparator) { - CALayer *separatorLayer = nil; - if (separatorIndex < self.itemSeparatorLayers.count) { - separatorLayer = self.itemSeparatorLayers[separatorIndex]; + } + } + return nil; +} + +- (void)handleItemViewEvent:(UIControl *)itemView { + NSIndexPath *indexPath = [self indexPathForItem:itemView.item]; + if (!indexPath) { + NSAssert(NO, @"the indexPath for the item could not be found"); + return; + } + + if (self.allowsSelection) { + BOOL shouldSelectItem = YES; + if (self.shouldSelectItemBlock) { + shouldSelectItem = self.shouldSelectItemBlock(itemView.item, itemView, indexPath.section, indexPath.row); + } + if (shouldSelectItem) { + NSMutableArray *selectedIndexPaths = self.selectedItemIndexPaths ? self.selectedItemIndexPaths.mutableCopy : [[NSMutableArray alloc] init]; + if (self.allowsMultipleSelection) { + if (itemView.selected) { + [selectedIndexPaths removeObject:indexPath]; } else { - separatorLayer = [CALayer qmui_separatorLayer]; - [self.scrollView.layer addSublayer:separatorLayer]; - [self.itemSeparatorLayers addObject:separatorLayer]; + [selectedIndexPaths addObject:indexPath]; } - separatorLayer.hidden = NO; - separatorLayer.backgroundColor = self.itemSeparatorColor.CGColor; - separatorIndex++; - } - - globalItemIndex++; - } - - BOOL shouldShowSectionSeparator = self.shouldShowSectionSeparator && section < sectionCount - 1; - if (shouldShowSectionSeparator) { - CALayer *separatorLayer = nil; - if (section < self.sectionSeparatorLayers.count) { - separatorLayer = self.sectionSeparatorLayers[section]; } else { - separatorLayer = [CALayer qmui_separatorLayer]; - [self.scrollView.layer addSublayer:separatorLayer]; - [self.sectionSeparatorLayers addObject:separatorLayer]; + // 单选,得把其他选中都清除 + [selectedIndexPaths removeAllObjects]; + if (!itemView.selected) { + [selectedIndexPaths addObject:indexPath]; + } } - separatorLayer.hidden = NO; - separatorLayer.backgroundColor = self.sectionSeparatorColor.CGColor; + self.selectedItemIndexPaths = selectedIndexPaths.copy; } } + + if (itemView.item.handler) { + itemView.item.handler(itemView.item, itemView, indexPath.section, indexPath.row); + } } -- (void)setItemSeparatorInset:(UIEdgeInsets)itemSeparatorInset { - _itemSeparatorInset = itemSeparatorInset; - [self setNeedsLayout]; +- (void)setBottomAccessoryView:(__kindof UIView *)bottomAccessoryView { + if (bottomAccessoryView != _bottomAccessoryView) { + [_bottomAccessoryView removeFromSuperview]; + } + _bottomAccessoryView = bottomAccessoryView; + [self.contentView addSubview:_bottomAccessoryView]; + [self setNeedsUpdateLayout]; } -- (void)setItemSeparatorColor:(UIColor *)itemSeparatorColor { - _itemSeparatorColor = itemSeparatorColor; - [self.itemSeparatorLayers enumerateObjectsUsingBlock:^(CALayer * _Nonnull layer, NSUInteger idx, BOOL * _Nonnull stop) { - layer.backgroundColor = itemSeparatorColor.CGColor; - }]; +- (void)tintColorDidChange { + [super tintColorDidChange]; + [self setNeedsReload]; } -- (void)setSectionSeparatorInset:(UIEdgeInsets)sectionSeparatorInset { - _sectionSeparatorInset = sectionSeparatorInset; - [self setNeedsLayout]; +- (NSString *)reuseIdentifierAtIndexPath:(NSIndexPath *)indexPath forType:(NSInteger)type { + if (self.shouldReuseItems) { + return @[@"cell", @"header", @"footer"][type]; + } + if (type == 0) { + QMUIPopupMenuItem *item = self.itemSections[indexPath.section][indexPath.row]; + return [NSString stringWithFormat:@"cell_%p", item]; + } + if (type == 1) { + return [NSString stringWithFormat:@"header_%p", self.itemSections[indexPath.section]]; + } + if (type == 2) { + return [NSString stringWithFormat:@"footer_%p", self.itemSections[indexPath.section]]; + } + return nil; } -- (void)setSectionSeparatorColor:(UIColor *)sectionSeparatorColor { - _sectionSeparatorColor = sectionSeparatorColor; - [self.sectionSeparatorLayers enumerateObjectsUsingBlock:^(CALayer * _Nonnull layer, NSUInteger idx, BOOL * _Nonnull stop) { - layer.backgroundColor = sectionSeparatorColor.CGColor; - }]; +#pragma mark - + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return self.itemSections.count; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.itemSections[section].count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + NSString *identifier = [self reuseIdentifierAtIndexPath:indexPath forType:0]; + QMUIPopupMenuCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; + if (!cell) { + cell = [[QMUIPopupMenuCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; + } + if (!cell.itemView) { + UIControl *itemView = nil; + if (self.itemViewGenerator) { + itemView = self.itemViewGenerator(self); + } else { + itemView = [[QMUIPopupMenuItemView alloc] init]; + } + cell.itemView = itemView; + } + + cell.itemView.tintColor = self.tintColor; + + QMUITableViewCellPosition position = [tableView qmui_positionForRowAtIndexPath:indexPath]; + if (self.shouldShowItemSeparator && !(position & QMUITableViewCellPositionLastInSection)) { + cell.qmui_borderPosition = QMUIViewBorderPositionBottom; + cell.qmui_borderWidth = self.itemSeparatorHeight; + cell.qmui_borderInsets = UIEdgeInsetsMake(self.itemSeparatorInset.bottom, self.itemSeparatorInset.right, self.itemSeparatorInset.top, self.itemSeparatorInset.left); + cell.qmui_borderColor = self.itemSeparatorColor; + } else { + cell.qmui_borderWidth = 0; + cell.qmui_borderPosition = QMUIViewBorderPositionNone; + } + + QMUIPopupMenuItem *item = self.itemSections[indexPath.section][indexPath.row]; + cell.itemView.item = item; + [cell.itemView addTarget:self action:@selector(handleItemViewEvent:) forControlEvents:UIControlEventTouchUpInside]; + + if ([self.selectedItemIndexPaths containsObject:indexPath]) { + cell.itemView.selected = YES; + } else { + cell.itemView.selected = NO; + } + + // 这个 block 是给业务自定义的机会,所以要放在最后面才能覆盖 + if (self.itemViewConfigurationHandler) { + self.itemViewConfigurationHandler(self, item, cell.itemView, indexPath.section, indexPath.row); + } + + if (item.configurationBlock) { + item.configurationBlock(item, cell.itemView, indexPath.section, indexPath.row); + } + + return cell; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + QMUIPopupMenuItem *item = self.itemSections[indexPath.section][indexPath.row]; + if (item.height == QMUIViewSelfSizingHeight) { + return UITableViewAutomaticDimension; + } + if (item.height >= 0 || self.itemHeight != QMUIViewSelfSizingHeight) { + CGFloat height = item.height >= 0 ? item.height : self.itemHeight; + QMUITableViewCellPosition position = [tableView qmui_positionForRowAtIndexPath:indexPath]; + if (self.shouldShowItemSeparator && !(position & QMUITableViewCellPositionLastInSection)) { + height += self.itemSeparatorHeight; + } + return height; + } + return UITableViewAutomaticDimension;// self.itemHeight == QMUIViewSelfSizingHeight +} + +- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { + if (section >= self.sectionTitles.count) return nil; + NSString *string = self.sectionTitles[section]; + if (!string.length) return nil; + NSString *identifier = [self reuseIdentifierAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section] forType:1]; + QMUIPopupMenuSectionHeaderView *header = [tableView dequeueReusableHeaderFooterViewWithIdentifier:identifier]; + if (!header) { + header = [[QMUIPopupMenuSectionHeaderView alloc] initWithReuseIdentifier:identifier]; + } + header.label.text = string; + if (self.sectionTitleConfigurationHandler) { + self.sectionTitleConfigurationHandler(self, header.label, section); + } + return header; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { + if (section >= self.sectionTitles.count) return CGFLOAT_MIN; + NSString *string = self.sectionTitles[section]; + if (!string.length) return CGFLOAT_MIN; + return UITableViewAutomaticDimension; +} + +- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section { + BOOL shouldShowSectionSeparator = self.shouldShowSectionSeparator && self.sectionSeparatorHeight; + BOOL shouldShowSectionFooter = shouldShowSectionSeparator || self.sectionSpacing > 0; + if (shouldShowSectionFooter && section != tableView.numberOfSections - 1) { + NSString *identifier = [self reuseIdentifierAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section] forType:2]; + QMUIPopupMenuSectionFooterView *footer = [tableView dequeueReusableHeaderFooterViewWithIdentifier:identifier]; + if (!footer) { + footer = [[QMUIPopupMenuSectionFooterView alloc] initWithReuseIdentifier:identifier]; + } + if (shouldShowSectionSeparator) { + footer.qmui_borderPosition = QMUIViewBorderPositionTop; + footer.qmui_borderWidth = self.sectionSeparatorHeight; + footer.qmui_borderColor = self.sectionSeparatorColor; + footer.qmui_borderInsets = self.sectionSeparatorInset; + } else { + footer.qmui_borderPosition = QMUIViewBorderPositionNone; + } + if (self.sectionSpacing > 0) { + footer.backgroundColor = self.sectionSpacingColor; + } else { + footer.backgroundColor = nil; + } + return footer; + } + return nil; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { + if (section == tableView.numberOfSections - 1) { + return CGFLOAT_MIN; + } + CGFloat height = 0; + if (self.shouldShowSectionSeparator && self.sectionSeparatorHeight) { + height += self.sectionSeparatorHeight; + } + if (self.sectionSpacing > 0) { + height += self.sectionSpacing; + } + return height > 0 ? height : CGFLOAT_MIN; } #pragma mark - (UISubclassingHooks) - (void)didInitialize { [super didInitialize]; + _adjustsWidthAutomatically = YES; + _selectedItemIndex = NSNotFound; self.contentEdgeInsets = UIEdgeInsetsZero; - self.scrollView = [[UIScrollView alloc] init]; - self.scrollView.scrollsToTop = NO; - self.scrollView.showsHorizontalScrollIndicator = NO; - self.scrollView.showsVerticalScrollIndicator = NO; - if (@available(iOS 11, *)) { - self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } - [self.contentView addSubview:self.scrollView]; - - self.itemSeparatorLayers = [[NSMutableArray alloc] init]; - self.sectionSeparatorLayers = [[NSMutableArray alloc] init]; + _tableView = [[QMUITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped]; + self.tableView.scrollsToTop = NO; + self.tableView.alwaysBounceHorizontal = NO; + self.tableView.alwaysBounceVertical = NO; + self.tableView.showsHorizontalScrollIndicator = NO; + self.tableView.showsVerticalScrollIndicator = NO; + self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + self.tableView.backgroundColor = nil; + self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, CGFLOAT_MIN)]; + self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, CGFLOAT_MIN)];// 避免尾部出现20pt空白 + self.tableView.backgroundView = UIView.new; + self.tableView.estimatedRowHeight = self.itemHeight; + self.tableView.estimatedSectionHeaderHeight = 20; + self.tableView.dataSource = self; + self.tableView.delegate = self; + [self.contentView addSubview:self.tableView]; [self updateAppearanceForPopupMenuView]; } - (CGSize)sizeThatFitsInContentView:(CGSize)size { - __block CGFloat width = 0; - __block CGFloat height = UIEdgeInsetsGetVerticalValue(self.padding); - [self.itemSections qmui_enumerateNestedArrayWithBlock:^(__kindof QMUIPopupMenuBaseItem *item, BOOL *stop) { - height += item.height >= 0 ? item.height : self.itemHeight; - CGSize itemSize = [item sizeThatFits:CGSizeMake(size.width, height)]; - width = MAX(width, MIN(itemSize.width, size.width)); - }]; - size.width = width; - size.height = MIN(height, size.height); - return size; + __block CGSize result = [self.tableView sizeThatFits:CGSizeMake(size.width, CGFLOAT_MAX)]; + if (self.adjustsWidthAutomatically) { + self.tableView.frame = CGRectMakeWithSize(result); + [self.tableView layoutIfNeeded]; + result = CGSizeZero; + [self.itemSections enumerateObjectsUsingBlock:^(NSArray<__kindof QMUIPopupMenuItem *> * _Nonnull sectionItems, NSUInteger section, BOOL * _Nonnull aStop) { + if (self.sectionTitles.count > section && self.sectionTitles[section].length) { + QMUIPopupMenuSectionHeaderView *header = (QMUIPopupMenuSectionHeaderView *)[self.tableView headerViewForSection:section]; + CGSize headerSize = [header sizeThatFits:CGSizeMake(size.width, CGFLOAT_MAX)]; + result.height += headerSize.height; + result.width = MAX(result.width, MIN(headerSize.width, size.width)); + } + [sectionItems enumerateObjectsUsingBlock:^(__kindof QMUIPopupMenuItem * _Nonnull rowItem, NSUInteger row, BOOL * _Nonnull bStop) { + QMUIPopupMenuCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:section]]; + CGSize itemSize = [cell.itemView sizeThatFits:CGSizeMake(size.width, CGFLOAT_MAX)]; + CGFloat itemHeight = rowItem.height; + if (itemHeight < 0) { + itemHeight = self.itemHeight; + } + // QMUIViewSelfSizingHeight + if (isinf(itemHeight)) { + itemHeight = itemSize.height; + } + if (self.shouldShowItemSeparator) { + itemHeight += self.itemSeparatorHeight;// 每个 section 结尾的那个 item 不需要算分隔线高度,在下文减去 + } + result.height += itemHeight; + result.width = MAX(result.width, MIN(itemSize.width, size.width)); + }]; + }]; + result.height += (self.itemSections.count - 1) * self.sectionSpacing; + if (self.shouldShowSectionSeparator) { + result.height += (self.itemSections.count - 1) * self.sectionSeparatorHeight; + } + if (self.shouldShowItemSeparator) { + result.height -= self.itemSections.count * self.itemSeparatorHeight;// 减去每个 section 结尾的那个 item 的分隔线 + } + } + if (self.bottomAccessoryView) { + CGSize accessoryViewSize = [self.bottomAccessoryView sizeThatFits:CGSizeMake(size.width, CGFLOAT_MAX)]; + result.height += accessoryViewSize.height; + } + result.height += UIEdgeInsetsGetVerticalValue(self.padding);// contentInset 不在系统 sizeThatFits: 返回结果内,要自己加 + return result; } - (void)layoutSubviews { [super layoutSubviews]; - self.scrollView.frame = self.contentView.bounds; - - CGFloat minY = self.padding.top; - CGFloat contentWidth = CGRectGetWidth(self.scrollView.bounds); - NSInteger separatorIndex = 0; - for (NSInteger section = 0, sectionCount = self.itemSections.count; section < sectionCount; section ++) { - NSArray *items = self.itemSections[section]; - for (NSInteger row = 0, rowCount = items.count; row < rowCount; row ++) { - QMUIPopupMenuBaseItem *item = items[row]; - item.frame = CGRectMake(0, minY, contentWidth, item.height >= 0 ? item.height : self.itemHeight); - minY = CGRectGetMaxY(item.frame); - - if (self.shouldShowItemSeparator && row < rowCount - 1) { - CALayer *layer = self.itemSeparatorLayers[separatorIndex]; - if (!layer.hidden) { - layer.frame = CGRectMake(self.padding.left + self.itemSeparatorInset.left, minY - PixelOne + self.itemSeparatorInset.top - self.itemSeparatorInset.bottom, contentWidth - UIEdgeInsetsGetHorizontalValue(self.padding) - UIEdgeInsetsGetHorizontalValue(self.itemSeparatorInset), PixelOne); - separatorIndex++; - } - } - } - - if (self.shouldShowSectionSeparator && section < sectionCount - 1) { - self.sectionSeparatorLayers[section].frame = CGRectMake(0, minY - PixelOne + self.sectionSeparatorInset.top - self.sectionSeparatorInset.bottom, contentWidth - UIEdgeInsetsGetHorizontalValue(self.sectionSeparatorInset), PixelOne); - } + CGRect contentRect = self.contentView.bounds; + if (self.bottomAccessoryView) { + CGSize accessoryViewSize = [self.bottomAccessoryView sizeThatFits:CGSizeMake(CGRectGetWidth(contentRect), CGFLOAT_MAX)]; + self.bottomAccessoryView.frame = CGRectMake(0, CGRectGetHeight(contentRect) - accessoryViewSize.height, CGRectGetWidth(contentRect), accessoryViewSize.height); + contentRect = CGRectSetHeight(contentRect, CGRectGetMinY(self.bottomAccessoryView.frame)); } - minY += self.padding.bottom; - self.scrollView.contentSize = CGSizeMake(contentWidth, minY); + self.tableView.frame = contentRect; } @end @@ -227,14 +575,16 @@ + (void)initialize { + (void)setDefaultAppearanceForPopupMenuView { QMUIPopupMenuView *appearance = [QMUIPopupMenuView appearance]; - appearance.shouldShowItemSeparator = NO; + appearance.shouldShowItemSeparator = YES; appearance.itemSeparatorColor = UIColorSeparator; appearance.itemSeparatorInset = UIEdgeInsetsZero; - appearance.shouldShowSectionSeparator = NO; + appearance.itemSeparatorHeight = PixelOne; + appearance.shouldShowSectionSeparator = YES; appearance.sectionSeparatorColor = UIColorSeparator; appearance.sectionSeparatorInset = UIEdgeInsetsZero; - appearance.itemTitleFont = UIFontMake(16); - appearance.itemTitleColor = UIColorBlue; + appearance.sectionSeparatorHeight = PixelOne; + appearance.sectionSpacing = 8; + appearance.sectionSpacingColor = UIColorSeparator; appearance.padding = UIEdgeInsetsMake([QMUIPopupContainerView appearance].cornerRadius / 2.0, 16, [QMUIPopupContainerView appearance].cornerRadius / 2.0, 16); appearance.itemHeight = 44; } @@ -244,11 +594,14 @@ - (void)updateAppearanceForPopupMenuView { self.shouldShowItemSeparator = appearance.shouldShowItemSeparator; self.itemSeparatorColor = appearance.itemSeparatorColor; self.itemSeparatorInset = appearance.itemSeparatorInset; + self.itemSeparatorHeight = appearance.itemSeparatorHeight; self.shouldShowSectionSeparator = appearance.shouldShowSectionSeparator; + self.sectionSeparatorHeight = appearance.sectionSeparatorHeight; self.sectionSeparatorColor = appearance.sectionSeparatorColor; self.sectionSeparatorInset = appearance.sectionSeparatorInset; - self.itemTitleFont = appearance.itemTitleFont; - self.itemTitleColor = appearance.itemTitleColor; + self.sectionSeparatorHeight = appearance.sectionSeparatorHeight; + self.sectionSpacing = appearance.sectionSpacing; + self.sectionSpacingColor = appearance.sectionSpacingColor; self.padding = appearance.padding; self.itemHeight = appearance.itemHeight; } diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.h b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.h index dde21502..28f78f25 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUINavigationBarScrollingAnimator.h @@ -89,7 +89,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nullable, nonatomic, copy) UIColor * (^tintColorBlock)(QMUINavigationBarScrollingAnimator * _Nonnull animator, float progress); /** - 返回不同滚动位置下对应的导航栏的 titleView tintColor + 返回不同滚动位置下对应的导航栏的 titleView tintColor,注意只能对使用了 navigationItem.titleView 生效(QMUICommonViewController 的子类默认用了 QMUINavigationTitleView,所以也可以生效)。 @param animator 当前的 animator 对象 @param progress 当前滚动位置处于 offsetYToStartAnimation 到 (offsetYToStartAnimation + distanceToStopAnimation) 之间的哪个进度 */ diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.m b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.m index e9800591..832dc7b5 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUINavigationBarScrollingAnimator.m @@ -16,6 +16,7 @@ #import "QMUINavigationBarScrollingAnimator.h" #import "UIViewController+QMUI.h" #import "UIScrollView+QMUI.h" +#import "UIView+QMUI.h" @interface QMUINavigationBarScrollingAnimator () @@ -34,16 +35,14 @@ - (instancetype)init { self.distanceToStopAnimation = 44; self.didScrollBlock = ^(QMUINavigationBarScrollingAnimator * _Nonnull animator) { - if (!animator.navigationBar) { - UINavigationBar *navigationBar = [QMUIHelper visibleViewController].navigationController.navigationBar; - if (navigationBar) { - animator.navigationBar = navigationBar; + UINavigationBar *navigationBar = animator.navigationBar; + if (!navigationBar) { + navigationBar = animator.scrollView.qmui_viewController.navigationController.navigationBar; + if (!navigationBar) { + NSLog(@"无法自动找到 UINavigationBar,或许此时 scrollView 所在的 viewController 已经不存在于 UINavigationController 里。"); + return; } } - if (!animator.navigationBar) { - NSLog(@"无法自动找到 UINavigationBar,请通过 %@.%@ 手动设置一个", NSStringFromClass(animator.class), NSStringFromSelector(@selector(navigationBar))); - return; - } CGFloat progress = animator.progress; @@ -58,31 +57,32 @@ - (instancetype)init { } else { if (animator.backgroundImageBlock) { UIImage *backgroundImage = animator.backgroundImageBlock(animator, progress); - [animator.navigationBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault]; + [navigationBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault]; } if (animator.shadowImageBlock) { UIImage *shadowImage = animator.shadowImageBlock(animator, progress); - animator.navigationBar.shadowImage = shadowImage; + navigationBar.shadowImage = shadowImage; } if (animator.tintColorBlock) { UIColor *tintColor = animator.tintColorBlock(animator, progress); - animator.navigationBar.tintColor = tintColor; + navigationBar.tintColor = tintColor; } if (animator.titleViewTintColorBlock) { UIColor *tintColor = animator.titleViewTintColorBlock(animator, progress); - animator.navigationBar.topItem.titleView.tintColor = tintColor;// TODO: 对 UIViewController 是否生效? + navigationBar.topItem.titleView.tintColor = tintColor; } if (animator.barTintColorBlock) { - animator.barTintColorBlock(animator, progress); + UIColor *barTintColor = animator.barTintColorBlock(animator, progress); + navigationBar.barTintColor = barTintColor; } if (animator.statusbarStyleBlock) { UIStatusBarStyle style = animator.statusbarStyleBlock(animator, progress); // 需在项目的 Info.plist 文件内设置字段 “View controller-based status bar appearance” 的值为 NO 才能生效,如果不设置,或者值为 YES,则请自行通过系统提供的 - preferredStatusBarStyle 方法来实现,statusbarStyleBlock 无效 BeginIgnoreDeprecatedWarning if (style >= UIStatusBarStyleLightContent) { - [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent]; + [UIApplication.sharedApplication setStatusBarStyle:UIStatusBarStyleLightContent]; } else { - [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault]; + [UIApplication.sharedApplication setStatusBarStyle:UIStatusBarStyleDefault]; } EndIgnoreDeprecatedWarning } @@ -95,7 +95,7 @@ - (instancetype)init { - (float)progress { UIScrollView *scrollView = self.scrollView; CGFloat contentOffsetY = flat(scrollView.contentOffset.y); - CGFloat offsetYToStartAnimation = flat(self.offsetYToStartAnimation + (self.adjustsOffsetYWithInsetTopAutomatically ? -scrollView.qmui_contentInset.top : 0)); + CGFloat offsetYToStartAnimation = flat(self.offsetYToStartAnimation + (self.adjustsOffsetYWithInsetTopAutomatically ? -scrollView.adjustedContentInset.top : 0)); if (contentOffsetY < offsetYToStartAnimation) { return 0; } diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.h b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.h index 45748159..afa8d4f7 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUINavigationBarScrollingSnapAnimator.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.m b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.m index 5be9d0bb..a79c42d6 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUINavigationBarScrollingSnapAnimator.m @@ -14,9 +14,9 @@ // #import "QMUINavigationBarScrollingSnapAnimator.h" -#import "UINavigationBar+QMUI.h" #import "UIViewController+QMUI.h" #import "UIScrollView+QMUI.h" +#import "UIView+QMUI.h" @interface QMUINavigationBarScrollingSnapAnimator () @@ -33,16 +33,14 @@ - (instancetype)init { self.adjustsOffsetYWithInsetTopAutomatically = YES; self.didScrollBlock = ^(QMUINavigationBarScrollingSnapAnimator * _Nonnull animator) { - if (!animator.navigationBar) { - UINavigationBar *navigationBar = [QMUIHelper visibleViewController].navigationController.navigationBar; - if (navigationBar) { - animator.navigationBar = navigationBar; + UINavigationBar *navigationBar = animator.navigationBar; + if (!navigationBar) { + navigationBar = animator.scrollView.qmui_viewController.navigationController.navigationBar; + if (!navigationBar) { + NSLog(@"无法自动找到 UINavigationBar,或许此时 scrollView 所在的 viewController 已经不存在于 UINavigationController 里。"); + return; } } - if (!animator.navigationBar) { - NSLog(@"无法自动找到 UINavigationBar,请通过 %@.%@ 手动设置一个", NSStringFromClass(animator.class), NSStringFromSelector(@selector(navigationBar))); - return; - } if (animator.animationBlock) { if (animator.offsetYReached) { @@ -67,7 +65,7 @@ - (instancetype)init { - (BOOL)offsetYReached { UIScrollView *scrollView = self.scrollView; CGFloat contentOffsetY = flat(scrollView.contentOffset.y); - CGFloat offsetYToStartAnimation = flat(self.offsetYToStartAnimation + (self.adjustsOffsetYWithInsetTopAutomatically ? -scrollView.qmui_contentInset.top : 0)); + CGFloat offsetYToStartAnimation = flat(self.offsetYToStartAnimation + (self.adjustsOffsetYWithInsetTopAutomatically ? -scrollView.adjustedContentInset.top : 0)); return contentOffsetY > offsetYToStartAnimation; } diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.h b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.h index 86853201..ab340d66 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIScrollAnimator.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.m b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.m index ad64099a..d3cbc73f 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIScrollAnimator.m diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISearchBar.h b/QMUI/QMUIKit/QMUIComponents/QMUISearchBar.h index dfe7dbaf..d6f6518a 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUISearchBar.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUISearchBar.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUISearchBar.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISearchBar.m b/QMUI/QMUIKit/QMUIComponents/QMUISearchBar.m index 6b937026..3b36769b 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUISearchBar.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUISearchBar.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUISearchBar.m diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISearchController.h b/QMUI/QMUIKit/QMUIComponents/QMUISearchController.h index 85b3259a..cc579338 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUISearchController.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUISearchController.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUISearchController.h @@ -16,6 +16,8 @@ #import "QMUICommonViewController.h" #import "QMUICommonTableViewController.h" +NS_ASSUME_NONNULL_BEGIN + @class QMUIEmptyView; @class QMUISearchController; @@ -32,7 +34,7 @@ * 搜索框文字发生变化时的回调,请自行调用 `[tableView reloadData]` 来更新界面。 * @warning 搜索框文字为空(例如第一次点击搜索框进入搜索状态时,或者文字全被删掉了,或者点击搜索框的×)也会走进来,此时参数searchString为@"",这是为了和系统的UISearchController保持一致 */ -- (void)searchController:(QMUISearchController *)searchController updateResultsForSearchString:(NSString *)searchString; +- (void)searchController:(QMUISearchController *)searchController updateResultsForSearchString:(nullable NSString *)searchString; @optional - (void)willPresentSearchController:(QMUISearchController *)searchController; @@ -50,27 +52,49 @@ * 1. 使用 initWithContentsViewController: 初始化 * 2. 通过 searchBar 属性得到搜索框的引用并直接使用,例如 `tableHeaderView = searchController.searchBar` * 3. 指定 searchResultsDelegate 属性并在其中实现 searchController:updateResultsForSearchString: 方法以更新搜索结果数据集 + * 4. 如果需要,可通过 @c qmui_preferredStatusBarStyleBlock 来控制搜索状态下的状态栏样式。 * * @note QMUICommonTableViewController 内部自带 QMUISearchController,只需将属性 shouldShowSearchBar 置为 YES 即可,无需自行初始化 QMUISearchController。 */ -@interface QMUISearchController : QMUICommonViewController +@interface QMUISearchController : QMUICommonViewController + +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE; +- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_UNAVAILABLE; + +- (instancetype)initWithContentsViewController:(UIViewController *)viewController resultsViewController:(__kindof UIViewController *)resultsViewController NS_DESIGNATED_INITIALIZER; + +/** + * 在某个指定的 UIViewController 上创建一个与其绑定的 searchController,并指定结果列表的 style。 + * @param viewController 要在哪个viewController上添加搜索功能 + */ +- (instancetype)initWithContentsViewController:(UIViewController *)viewController resultsTableViewStyle:(UITableViewStyle)resultsTableViewStyle; /** - * 在某个指定的UIViewController上创建一个与其绑定的searchController + * 在某个指定的 UIViewController 上创建一个与其绑定的 searchController * @param viewController 要在哪个viewController上添加搜索功能 */ - (instancetype)initWithContentsViewController:(UIViewController *)viewController; @property(nonatomic, weak) id searchResultsDelegate; +/// 内部使用的系统的 UISearchController 的引用 +@property(nonatomic, strong, readonly) UISearchController *searchController; + +/// 等价于 self.searchController.searchResultsController,展示搜索结果的 viewController。若通过 initWithContentsViewController:resultsTableViewStyle: 初始化,则默认的 searchResultsController 为 QMUICommonTableViewController 的子类。 +@property(nonatomic, strong, readonly, nullable) __kindof UIViewController *searchResultsController; + /// 搜索框 @property(nonatomic, strong, readonly) UISearchBar *searchBar; -/// 搜索结果列表 -@property(nonatomic, strong, readonly) QMUITableView *tableView; +/// 搜索结果列表,仅当通过 initWithContentsViewController: 或 initWithContentsViewController:resultsTableViewStyle: 初始化时才有效。 +@property(nonatomic, strong, readonly, nullable) QMUITableView *tableView; /// 在搜索文字为空时会展示的一个 view,通常用于实现“最近搜索”之类的功能。launchView 最终会被布局为撑满搜索框以下的所有空间。 -@property(nonatomic, strong) UIView *launchView; +@property(nonatomic, strong, nullable) UIView *launchView; + +/// 升起键盘时的半透明遮罩,nil 表示用系统的,非 nil 则用自己的。默认为 nil。 +/// @note 如果使用了 launchView 则该属性无效。 +@property(nonatomic, strong, nullable) UIColor *dimmingColor; /// 控制以无动画的形式进入/退出搜索状态 @property(nonatomic, assign, getter=isActive) BOOL active; @@ -84,6 +108,14 @@ /// 进入搜索状态时是否要把原界面的 navigationBar 推走,默认为 YES @property(nonatomic, assign) BOOL hidesNavigationBarDuringPresentation; + +/// 在展示搜索结果或者 launchView 时是否支持左侧屏幕边缘向右滑退出搜索,默认为 NO +/// @warning 使用截图的方式实现,所以暂不支持横竖屏切换,请自行屏蔽横竖屏场景 +@property(nonatomic, assign) BOOL supportsSwipeToDismissSearch; + +/// 当开启了 supportsSwipeToDismissSearch 则在 willPresentSearchController: 里会创建这个手势对象 +@property(nonatomic, strong, readonly, nullable) UIScreenEdgePanGestureRecognizer *swipeGestureRecognizer; + @end @@ -104,7 +136,7 @@ * * @see QMUITableViewDelegate */ -@property(nonatomic, strong, readonly) QMUISearchController *searchController; +@property(nonatomic, strong, readonly, nullable) QMUISearchController *searchController; /** * 获取当前的 searchBar,注意只有当 `shouldShowSearchBar` 为 `YES` 时才有用 @@ -113,7 +145,7 @@ * * @see QMUITableViewDelegate */ -@property(nonatomic, strong, readonly) UISearchBar *searchBar; +@property(nonatomic, strong, readonly, nullable) UISearchBar *searchBar; /** * 是否应该在显示空界面时自动隐藏搜索框 @@ -132,3 +164,5 @@ - (void)initSearchController; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISearchController.m b/QMUI/QMUIKit/QMUIComponents/QMUISearchController.m index c17865e0..0a77d8c2 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUISearchController.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUISearchController.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUISearchController.m @@ -23,6 +23,9 @@ #import "NSString+QMUI.h" #import "NSObject+QMUI.h" #import "UIView+QMUI.h" +#import "UIViewController+QMUI.h" +#import "UISearchController+QMUI.h" +#import "UIGestureRecognizer+QMUI.h" BeginIgnoreDeprecatedWarning @@ -42,90 +45,49 @@ @implementation QMUISearchResultsTableViewController - (void)initTableView { [super initTableView]; - if (@available(iOS 11, *)) { - self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } + + // 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]; } } -@end - -@interface QMUICustomSearchController : UISearchController - -@property(nonatomic, strong) UIView *customDimmingView; -@end - -@implementation QMUICustomSearchController - -- (void)setCustomDimmingView:(UIView *)customDimmingView { - if (_customDimmingView != customDimmingView) { - [_customDimmingView removeFromSuperview]; - } - _customDimmingView = customDimmingView; - - self.dimsBackgroundDuringPresentation = !_customDimmingView; - if ([self isViewLoaded]) { - [self addCustomDimmingView]; - } -} - -- (void)viewWillAppear:(BOOL)animated { - [super viewWillAppear:animated]; - [self addCustomDimmingView]; -} - -- (void)addCustomDimmingView { - UIView *superviewOfDimmingView = self.searchResultsController.view.superview; - if (self.customDimmingView && self.customDimmingView.superview != superviewOfDimmingView) { - [superviewOfDimmingView insertSubview:self.customDimmingView atIndex:0]; - [self layoutCustomDimmingView]; - } -} - -- (void)layoutCustomDimmingView { - UIView *searchBarContainerView = nil; - for (UIView *subview in self.view.subviews) { - if ([NSStringFromClass(subview.class) isEqualToString:@"_UISearchBarContainerView"]) { - searchBarContainerView = subview; - break; - } - } - - self.customDimmingView.frame = CGRectInsetEdges(self.customDimmingView.superview.bounds, UIEdgeInsetsMake(searchBarContainerView ? CGRectGetMaxY(searchBarContainerView.frame) : 0, 0, 0, 0)); -} - - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - - if (self.customDimmingView) { - [UIView animateWithDuration:[CATransaction animationDuration] animations:^{ - [self layoutCustomDimmingView]; - }]; + if ([self.delegate isKindOfClass:QMUISearchController.class]) { + QMUISearchController *searchController = (QMUISearchController *)self.delegate; + if (searchController.emptyViewShowing) { + [searchController layoutEmptyView]; + } } } @end -@interface QMUISearchController () - -@property(nonatomic,strong) QMUICustomSearchController *searchController; +@interface QMUISearchController () +@property(nonatomic, strong) UIView *snapshotView; +@property(nonatomic, strong) UIView *snapshotMaskView; +@property(nonatomic, assign) BOOL dismissBySwipe; +@property(nonatomic, assign) BOOL hasSetShowsCancelButton; @end @implementation QMUISearchController -- (instancetype)initWithContentsViewController:(UIViewController *)viewController { - if (self = [self initWithNibName:nil bundle:nil]) { +- (instancetype)initWithContentsViewController:(UIViewController *)viewController resultsViewController:(__kindof UIViewController *)resultsViewController { + if (self = [super initWithNibName:nil bundle:nil]) { // 将 definesPresentationContext 置为 YES 有两个作用: // 1、保证从搜索结果界面进入子界面后,顶部的searchBar不会依然停留在navigationBar上 // 2、使搜索结果界面的tableView的contentInset.top正确适配searchBar viewController.definesPresentationContext = YES; + [QMUISearchController fixDefinesPresentationContextBug]; - QMUISearchResultsTableViewController *searchResultsViewController = [[QMUISearchResultsTableViewController alloc] init]; - searchResultsViewController.delegate = self; - self.searchController = [[QMUICustomSearchController alloc] initWithSearchResultsController:searchResultsViewController]; + _searchController = [[UISearchController alloc] initWithSearchResultsController:resultsViewController]; + self.searchController.obscuresBackgroundDuringPresentation = YES;// iOS 15 开始该默认值为 NO 了,为了保持与旧版本一致的表现,这里改默认值 self.searchController.searchResultsUpdater = self; self.searchController.delegate = self; _searchBar = self.searchController.searchBar; @@ -140,6 +102,41 @@ - (instancetype)initWithContentsViewController:(UIViewController *)viewControlle return self; } +- (instancetype)initWithContentsViewController:(UIViewController *)viewController resultsTableViewStyle:(UITableViewStyle)resultsTableViewStyle { + QMUISearchResultsTableViewController *searchResultsViewController = [[QMUISearchResultsTableViewController alloc] initWithStyle:resultsTableViewStyle]; + if (self = [self initWithContentsViewController:viewController resultsViewController:searchResultsViewController]) { + searchResultsViewController.delegate = self; + } + return self; +} + +- (instancetype)initWithContentsViewController:(UIViewController *)viewController { + return [self initWithContentsViewController:viewController resultsTableViewStyle:UITableViewStylePlain]; +} + ++ (void)fixDefinesPresentationContextBug { + [QMUIHelper executeBlock:^{ + // 修复当处于搜索状态时被 -[UINavigationController popToRootViewControllerAnimated:] 强制切走界面可能引发内存泄露的问题 + // https://github.com/Tencent/QMUI_iOS/issues/1541 + OverrideImplementation([UIViewController class], @selector(didMoveToParentViewController:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIViewController *selfObject, UIViewController *parentViewController) { + + // call super + void (*originSelectorIMP)(id, SEL, UIViewController *); + originSelectorIMP = (void (*)(id, SEL, UIViewController *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, parentViewController); + + if (!parentViewController) { + if (selfObject.definesPresentationContext && selfObject.presentedViewController.presentingViewController == selfObject && [selfObject.presentedViewController isKindOfClass:UISearchController.class]) { + QMUILogWarn(@"QMUISearchController", @"fix #1541, didMoveToParent, %@", selfObject); + [selfObject dismissViewControllerAnimated:NO completion:nil]; + } + } + }; + }); + } oncePerIdentifier:@"QMUISearchController presentation"]; +} + - (void)viewDidLoad { [super viewDidLoad]; // 主动触发 loadView,如果不这么做,那么有可能直到 QMUISearchController 被销毁,这期间 self.searchController 都没有被触发 loadView,然后在 dealloc 时就会报错,提示尝试在释放 self.searchController 时触发了 self.searchController 的 loadView @@ -152,6 +149,11 @@ - (void)setSearchResultsDelegate:(id)searchResults self.tableView.delegate = _searchResultsDelegate; } +- (void)setDimmingColor:(UIColor *)dimmingColor { + _dimmingColor = dimmingColor; + self.searchController.qmui_dimmingColor = dimmingColor; +} + - (BOOL)isActive { return self.searchController.active; } @@ -161,16 +163,38 @@ - (void)setActive:(BOOL)active { } - (void)setActive:(BOOL)active animated:(BOOL)animated { - self.searchController.active = active; + if (!animated) { + [UIView performWithoutAnimation:^{ + self.searchController.active = active; + // animated:NO 的情况下设置 active:NO,取消按钮无法自动消失(系统 bug),所以这里手动管理 + // 如果是 animated:YES 或者 active:YES 则没这个问题 + // 这里修改了 searchBar.showsCancelButton 属性会让 automaticallyShowsCancelButton 变为 NO,且不能在这时候立马把它改为 YES,否则会立马出现取消按钮,所以改为在下一次 willPresentSearchController: 里重置为系统自动管理。 + if (!active && self.searchController.automaticallyShowsCancelButton) { + self.searchController.searchBar.showsCancelButton = NO; + self.hasSetShowsCancelButton = YES; + } + }]; + } else { + self.searchController.active = active; + } } - (UITableView *)tableView { - return ((QMUICommonTableViewController *)self.searchController.searchResultsController).tableView; + if ([self.searchResultsController respondsToSelector:@selector(tableView)]) { + BeginIgnorePerformSelectorLeaksWarning + return [self.searchResultsController performSelector:@selector(tableView)]; + EndIgnorePerformSelectorLeaksWarning + } + return nil; +} + +- (__kindof UIViewController *)searchResultsController { + return self.searchController.searchResultsController; } -- (void)setLaunchView:(UIView *)dimmingView { - _launchView = dimmingView; - self.searchController.customDimmingView = _launchView; +- (void)setLaunchView:(UIView *)launchView { + _launchView = launchView; + self.searchController.qmui_launchView = launchView; } - (BOOL)hidesNavigationBarDuringPresentation { @@ -181,6 +205,123 @@ - (void)setHidesNavigationBarDuringPresentation:(BOOL)hidesNavigationBarDuringPr self.searchController.hidesNavigationBarDuringPresentation = hidesNavigationBarDuringPresentation; } +- (void)setQmui_prefersStatusBarHiddenBlock:(BOOL (^)(void))qmui_prefersStatusBarHiddenBlock { + [super setQmui_prefersStatusBarHiddenBlock:qmui_prefersStatusBarHiddenBlock]; + self.searchController.qmui_prefersStatusBarHiddenBlock = qmui_prefersStatusBarHiddenBlock; +} + +- (void)setQmui_preferredStatusBarStyleBlock:(UIStatusBarStyle (^)(void))qmui_preferredStatusBarStyleBlock { + [super setQmui_preferredStatusBarStyleBlock:qmui_preferredStatusBarStyleBlock]; + self.searchController.qmui_preferredStatusBarStyleBlock = qmui_preferredStatusBarStyleBlock; +} + +- (void)handleSwipe:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer { + if (!self.launchView && (!self.searchController.searchResultsController.viewLoaded || self.searchController.searchResultsController.view.hidden)) return; + CGFloat snapshotInitialX = -112; + switch (gestureRecognizer.state) { + case UIGestureRecognizerStatePossible: + return; + case UIGestureRecognizerStateBegan: { + [self.searchController.view endEditing:YES]; + [self.searchController.view.superview insertSubview:self.snapshotView belowSubview:self.searchController.view]; + self.snapshotView.transform = CGAffineTransformMakeTranslation(snapshotInitialX, 0); + self.snapshotMaskView.alpha = 1; + QMUILogInfo(@"QMUISearchController", @"swipeGesture snapshot added to search view"); + } + return; + case UIGestureRecognizerStateChanged: { + CGFloat transition = MIN(MAX(0, [gestureRecognizer translationInView:gestureRecognizer.view].x), CGRectGetWidth(self.searchController.view.superview.bounds)); + self.searchController.view.transform = CGAffineTransformMakeTranslation(transition, 0); + double percent = transition / CGRectGetWidth(self.searchController.view.superview.bounds); + self.snapshotView.transform = CGAffineTransformMakeTranslation(snapshotInitialX * (1 - percent), 0); + self.snapshotMaskView.alpha = 1 - percent; + } + return; + case UIGestureRecognizerStateEnded: { + CGPoint velocity = [gestureRecognizer velocityInView:gestureRecognizer.view]; + if (CGRectGetMinX(self.searchController.view.frame) > CGRectGetWidth(self.searchController.view.superview.bounds) / 4 && velocity.x > 0) { + NSTimeInterval duration = 0.2 * (CGRectGetWidth(self.searchController.view.superview.bounds) - CGRectGetMinX(self.searchController.view.frame)) / CGRectGetWidth(self.searchController.view.superview.bounds); + [UIApplication.sharedApplication beginIgnoringInteractionEvents]; + [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + self.searchController.view.transform = CGAffineTransformMakeTranslation(CGRectGetWidth(self.searchController.view.superview.bounds), 0); + self.snapshotView.transform = CGAffineTransformIdentity; + self.snapshotMaskView.alpha = 0; + } completion:^(BOOL finished) { + self.dismissBySwipe = YES; + // 盖到最上面,挡住退出搜索过程中可能出现的界面闪烁 + [self.snapshotView removeFromSuperview]; + [UIApplication.sharedApplication.delegate.window addSubview:self.snapshotView]; + QMUILogInfo(@"QMUISearchController", @"swipeGesture snapshot change superview to window"); + self.active = NO; + self.searchController.view.transform = CGAffineTransformIdentity; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self cleanSnapshotObjects]; + self.dismissBySwipe = NO; + [UIApplication.sharedApplication endIgnoringInteractionEvents]; + }); + }]; + return; + } + } + default: + break; + } + + // reset to active:YES + [UIApplication.sharedApplication beginIgnoringInteractionEvents]; + NSTimeInterval duration = 0.2 * CGRectGetMinX(self.searchController.view.frame) / CGRectGetWidth(self.searchController.view.superview.bounds); + [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + self.searchController.view.transform = CGAffineTransformIdentity; + self.snapshotView.transform = CGAffineTransformMakeTranslation(snapshotInitialX, 0); + self.snapshotMaskView.alpha = 1; + } completion:^(BOOL finished) { + [UIApplication.sharedApplication endIgnoringInteractionEvents]; + QMUILogInfo(@"QMUISearchController", @"swipeGesture cancelled"); + }]; +} + +- (void)createSnapshotObjects { + if (!self.snapshotMaskView) { + self.snapshotMaskView = [[UIView alloc] init]; + self.snapshotMaskView.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:.1]; + } + self.snapshotView = [UIApplication.sharedApplication.delegate.window snapshotViewAfterScreenUpdates:NO]; + self.snapshotMaskView.frame = self.snapshotView.bounds; + [self.snapshotView addSubview:self.snapshotMaskView]; + if (!self.swipeGestureRecognizer) { + _swipeGestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)]; + self.swipeGestureRecognizer.edges = UIRectEdgeLeft; + self.swipeGestureRecognizer.delegate = self; + } + [UIApplication.sharedApplication.delegate.window addGestureRecognizer:self.swipeGestureRecognizer]; +} + +- (void)resetSnapshotObjects { + self.snapshotView.transform = CGAffineTransformIdentity; + [self.snapshotView removeFromSuperview]; +} + +- (void)cleanSnapshotObjects { + [self.snapshotView removeFromSuperview]; + [self.snapshotMaskView removeFromSuperview]; + self.snapshotView = nil; + [UIApplication.sharedApplication.delegate.window removeGestureRecognizer:self.swipeGestureRecognizer]; + QMUILogInfo(@"QMUISearchController", @"swipeGesture clean all objects"); +} + +#pragma mark - + +// 由于手势是加在 window 上的,所以任何时候都可能被触发(比如在搜索结果里弹出 toast 或 present 到新的界面),所以这里要做保护,只有在搜索结果肉眼可见的情况下才响应手势 +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer == self.swipeGestureRecognizer) { + UIView *targetView = [gestureRecognizer qmui_targetView]; + if (![targetView isDescendantOfView:self.searchController.view]) { + return NO; + } + } + return YES; +} + #pragma mark - QMUIEmptyView - (void)showEmptyView { @@ -202,7 +343,7 @@ - (void)showEmptyView { UIView *superview = self.searchController.searchResultsController.view; [superview addSubview:self.emptyView]; } else { - NSAssert(NO, @"QMUISearchController无法为emptyView找到合适的superview"); + QMUIAssert(NO, NSStringFromClass(self.class), @"QMUISearchController 无法为 emptyView 找到合适的 superview"); } [self layoutEmptyView]; @@ -219,6 +360,11 @@ - (void)didLoadTableViewInSearchResultsTableViewController:(QMUISearchResultsTab #pragma mark - - (void)updateSearchResultsForSearchController:(UISearchController *)searchController { + // 先触发手势返回再取消,从而让截图添加到屏幕上。然后再点搜索框的×按钮清空列表,此时要保证背后的截图也一起去除 + NSString *text = searchController.searchBar.text; + if (self.supportsSwipeToDismissSearch && !text.length && !searchController.qmui_alwaysShowSearchResultsController) { + [self resetSnapshotObjects]; + } if ([self.searchResultsDelegate respondsToSelector:@selector(searchController:updateResultsForSearchString:)]) { [self.searchResultsDelegate searchController:self updateResultsForSearchString:searchController.searchBar.text]; } @@ -227,6 +373,20 @@ - (void)updateSearchResultsForSearchController:(UISearchController *)searchContr #pragma mark - - (void)willPresentSearchController:(UISearchController *)searchController { + if (self.supportsSwipeToDismissSearch) { + [self createSnapshotObjects]; + QMUILogInfo(@"QMUISearchController", @"swipeGesture added"); + } + + // 走到这里意味着曾经因为 setActive:NO animated:NO 而不得不手动修改 searchBar.showsCancelButton 属性,导致 automaticallyShowsCancelButton 为 NO,系统无法自动显示取消按钮,所以这里在进入搜索前恢复自动管理 + if (self.hasSetShowsCancelButton) { + self.searchController.automaticallyShowsCancelButton = YES; + self.hasSetShowsCancelButton = NO; + } + + if (self.searchController.qmui_prefersStatusBarHiddenBlock || self.searchController.qmui_preferredStatusBarStyleBlock) { + [self.searchController setNeedsStatusBarAppearanceUpdate]; + } if ([self.searchResultsDelegate respondsToSelector:@selector(willPresentSearchController:)]) { [self.searchResultsDelegate willPresentSearchController:self]; } @@ -239,9 +399,17 @@ - (void)didPresentSearchController:(UISearchController *)searchController { } - (void)willDismissSearchController:(UISearchController *)searchController { + if (self.searchController.qmui_prefersStatusBarHiddenBlock || self.searchController.qmui_preferredStatusBarStyleBlock) { + [self.searchController setNeedsStatusBarAppearanceUpdate]; + } if ([self.searchResultsDelegate respondsToSelector:@selector(willDismissSearchController:)]) { [self.searchResultsDelegate willDismissSearchController:self]; } + + // 先手势返回触发各种对象的初始化,然后又取消手势,正常点取消按钮退出搜索,此时就不应该看到背后有截图存在了 + if (!self.dismissBySwipe) { + [self cleanSnapshotObjects]; + } } - (void)didDismissSearchController:(UISearchController *)searchController { @@ -251,6 +419,10 @@ - (void)didDismissSearchController:(UISearchController *)searchController { if ([self.searchResultsDelegate respondsToSelector:@selector(didDismissSearchController:)]) { [self.searchResultsDelegate didDismissSearchController:self]; } + + if (self.supportsSwipeToDismissSearch && !self.dismissBySwipe) { + [self cleanSnapshotObjects]; + } } @end @@ -326,7 +498,7 @@ - (BOOL)shouldShowSearchBar { - (void)initSearchController { if ([self isViewLoaded] && self.shouldShowSearchBar && !self.searchController) { - self.searchController = [[QMUISearchController alloc] initWithContentsViewController:self]; + self.searchController = [[QMUISearchController alloc] initWithContentsViewController:self resultsTableViewStyle:self.tableView.style]; self.searchController.searchResultsDelegate = self; self.searchController.searchBar.placeholder = @"搜索"; self.searchController.searchBar.qmui_usedAsTableHeaderView = YES;// 以 tableHeaderView 的方式使用 searchBar 的话,将其置为 YES,以辅助兼容一些系统 bug @@ -346,3 +518,19 @@ - (void)searchController:(QMUISearchController *)searchController updateResultsF } @end + +@implementation UINavigationController (Search) + +// 修复当处于搜索状态时被 window.rootViewController = xxx 强制切走界面可能引发内存泄露的问题 +// 这种场景会调用 nav 的 dealloc 但不会触发 child 的 didMoveToParentViewController:,所以只能重写 dealloc 处理一遍 +// https://github.com/Tencent/QMUI_iOS/issues/1541 +- (void)dealloc { + [self.childViewControllers.copy enumerateObjectsUsingBlock:^(__kindof UIViewController * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (obj.definesPresentationContext && obj.presentedViewController.presentingViewController == obj && [obj.presentedViewController isKindOfClass:UISearchController.class]) { + QMUILogWarn(@"QMUISearchController", @"fix #1541, dealloc, %@", obj); + [obj dismissViewControllerAnimated:NO completion:nil]; + } + }]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISegmentedControl.h b/QMUI/QMUIKit/QMUIComponents/QMUISegmentedControl.h index 57c04651..20c389d2 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUISegmentedControl.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUISegmentedControl.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUISegmentedControl.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISegmentedControl.m b/QMUI/QMUIKit/QMUIComponents/QMUISegmentedControl.m index 43bd0000..2381a4d6 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUISegmentedControl.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUISegmentedControl.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUISegmentedControl.m diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationNavigationBar.h b/QMUI/QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationNavigationBar.h new file mode 100644 index 00000000..af674424 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationNavigationBar.h @@ -0,0 +1,21 @@ +// +// QMUISheetPresentationNavigationBar.h +// QMUIKit +// +// Created by molice on 2024/2/27. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface QMUISheetPresentationNavigationBar : UIView + +@property(nonatomic, strong, nullable) UINavigationItem *navigationItem; + +@property(nonatomic, strong) UILabel *titleLabel; +@property(nonatomic, strong) __kindof UIView *titleView; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationNavigationBar.m b/QMUI/QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationNavigationBar.m new file mode 100644 index 00000000..8109d242 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationNavigationBar.m @@ -0,0 +1,61 @@ +// +// QMUISheetPresentationNavigationBar.m +// QMUIKit +// +// Created by molice on 2024/2/27. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import "QMUISheetPresentationNavigationBar.h" +#import "QMUICore.h" +#import "QMUIButton.h" +#import "QMUINavigationButton.h" + +@interface QMUISheetPresentationNavigationBar () +@property(nonatomic, strong) QMUINavigationButton *backButton; +@property(nonatomic, strong) QMUIButton *leftButton; +@property(nonatomic, strong) QMUIButton *rightButton; +@end + +@implementation QMUISheetPresentationNavigationBar + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.backgroundColor = UIColor.whiteColor; + + self.titleLabel = [[UILabel alloc] init]; + if (QMUICMIActivated) { + self.titleLabel.font = NavBarTitleFont; + self.titleLabel.textColor = NavBarTitleColor; + } + } + return self; +} + +- (void)setNavigationItem:(UINavigationItem *)navigationItem { + if (_navigationItem != navigationItem) { + self.titleLabel.text = nil; + [self.titleView removeFromSuperview]; + } + _navigationItem = navigationItem; + if (navigationItem.titleView) { + self.titleView = navigationItem.titleView; + } else if (navigationItem.title.length) { + self.titleLabel.text = navigationItem.title; + self.titleView = self.titleLabel; + } + [self addSubview:self.titleView]; +} + +- (CGSize)sizeThatFits:(CGSize)size { + return CGSizeMake(size.width, 56); +} + +- (void)layoutSubviews { + [super layoutSubviews]; + [self.titleView sizeToFit]; + self.titleView.center = CGPointMake(CGRectGetWidth(self.bounds) / 2, CGRectGetHeight(self.bounds) / 2); +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationSupports.h b/QMUI/QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationSupports.h new file mode 100644 index 00000000..e68d6eaf --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationSupports.h @@ -0,0 +1,79 @@ +// +// QMUISheetPresentationSupports.h +// QMUIKit +// +// Created by molice on 2024/2/27. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import +#import +#import "QMUINavigationController.h" + +NS_ASSUME_NONNULL_BEGIN + +@class QMUISheetPresentationNavigationBar; + +/// 当某个界面以半屏浮层方式显示时,可通过 vc.qmui_sheetPresentation 获取该界面的半屏浮层配置对象,通过该对象来修改浮层的样式、行为。 +/// 业务不应该自己构造一个新实例。 +@interface QMUISheetPresentation : NSObject + +/// 弹出时背后的遮罩颜色,默认为 UIColorMask(若有使用配置表)或 0.35 alpha 的黑色,可通过设为 nil 来去除遮罩。 +@property(nonatomic, strong, nullable) UIColor *dimmingColor; + +/// 是否模态弹出,YES 表示点击遮罩无响应,NO 表示点击遮罩自动关闭面板。默认为 NO。当设置为 YES 时也会同时屏蔽 swipe、pull 手势(你可以手动再打开)。 +@property(nonatomic, assign) BOOL modal; + +/// 是否支持侧滑关闭面板,默认为 YES。 +@property(nonatomic, assign) BOOL supportsSwipeToDismiss; + +/// 是否支持下拉关闭面板,默认为 YES。 +@property(nonatomic, assign) BOOL supportsPullToDismiss; + +/// 是否需要显示浮层顶部的仿原生导航栏(可自动显示 vc.title、vc.navigationItem 按钮),默认为 YES。 +@property(nonatomic, assign) BOOL shouldShowNavigationBar; + +/// 浮层左上角、右上角的圆角值,默认为10。 +@property(nonatomic, assign) CGFloat cornerRadius; + +/// 计算当前浮层在给定宽高下的内容大小,若希望表达无限制,则使用 CGFLOAT_MAX。 +/// 业务不需要考虑 navigationBar、safeAreaInsets,组件会自己加上。 +/// 也不需要考虑最大最小值保护,组件会自己处理。 +/// 若不设置则使用默认宽高(高度固定200pt)。 +@property(nonatomic, copy, nullable) CGSize (^preferredSheetContentSizeBlock)(QMUISheetPresentation *aSheetPresentation, CGSize aContainerSize); + +- (instancetype)init NS_UNAVAILABLE; +@end + +@interface UIViewController (QMUISheetSupports) + +/// 是否以 QMUISheetPresented 方式展示,在 viewDidLoad 及以后的时机都可以使用。 +/// @warning qmui_isPresentedInSheet 为 YES 的情况下,qmui_isPresented 为 NO,请注意区分这两者。 +@property(nonatomic, assign, readonly) BOOL qmui_isPresentedInSheet; + +/// 用于配置当前半屏浮层效果的对象,懒加载,业务如需修改值,直接访问并设置即可。 +/// 注意如果当前界面并非使用半屏浮层方式显示,这个属性依然会返回值。 +@property(nonatomic, strong, readonly) QMUISheetPresentation *qmui_sheetPresentation; + +/// 获取当前浮层里的仿原生导航栏,可对其进行样式、内容等设置,一般在 viewWillAppear: 时进行。 +@property(nonatomic, strong, readonly) QMUISheetPresentationNavigationBar *qmui_sheetPresentationNavigationBar; + +/// 当前浮层的侧滑手势对象,在 viewWillAppear: 及以后的时机都可以使用,业务可以自行修改 .delegate = xxx,但所有方法均需使用 QMUISheetPresentation.supportsSwipeToDismiss 值来判断当前手势是否有效。 +@property(nonatomic, strong, readonly) UIScreenEdgePanGestureRecognizer *qmui_sheetPresentationSwipeGestureRecognizer; + +/// 当前浮层的下拉手势对象,在 viewWillAppear: 及以后的时机都可以使用,业务可以自行修改 .delegate = xxx,但所有方法均需使用 QMUISheetPresentation.supportsPullToDismiss 值来判断当前手势是否有效。 +@property(nonatomic, strong, readonly) UIPanGestureRecognizer *qmui_sheetPresentationPullGestureRecognizer; + +/// 必要时业务可通过该方法主动刷新浮层布局,内部会自动判断当前若正在显示浮层,则以动画形式刷新布局,否则在下一个 runloop 才刷新。 +- (void)qmui_invalidateSheetPresentationLayout; +@end + +@interface QMUINavigationController (QMUISheetSupports) + +/// 将指定界面放到一个导航容器里并以半屏浮层的形式显示出来,浮层的样式、尺寸可通过 rootViewController.qmui_sheetPresentation 来配置。 +/// 构造完直接用系统的 present 方法把返回值显示出来即可。 +/// rootViewController 内部可用标准的 self.navigationController pushXxx/popXxx 写法来切换界面。 +- (instancetype)qmui_initWithSheetRootViewController:(UIViewController *)rootViewController; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationSupports.m b/QMUI/QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationSupports.m new file mode 100644 index 00000000..2af2936f --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationSupports.m @@ -0,0 +1,414 @@ +// +// QMUISheetPresentationSupports.m +// QMUIKit +// +// Created by molice on 2024/2/27. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import "QMUISheetPresentationSupports.h" +#import "QMUICore.h" +#import "UIView+QMUI.h" +#import "UIViewController+QMUI.h" +#import "QMUISheetPresentationNavigationBar.h" +#import "QMUIMultipleDelegates.h" + +// QMUISheet 模式下升起半屏的导航时,专用于存放第一个 vc 的带半透明背景的容器,由它负责决定业务 vc 的半屏布局 +@interface QMUISheetRootContainerViewController : UIViewController +@property(nonatomic, strong, readonly) UIControl *dimmingControl; +@property(nonatomic, strong, readonly) UIView *containerView; +@property(nonatomic, strong, readonly) QMUISheetPresentationNavigationBar *navigationBar; +@property(nonatomic, strong, readonly) UIViewController *rootViewController; + +@property(nonatomic, strong) UIPercentDrivenInteractiveTransition *interactiveTransition; +@property(nonatomic, strong) UIScreenEdgePanGestureRecognizer *edgePan; +@property(nonatomic, strong) UIPanGestureRecognizer *pullPan; + +@property(nonatomic, assign) BOOL shouldPerformPresentAnimation; +- (void)layout; +@end + +@interface QMUISheetRootControllerAnimator : NSObject +@property(nonatomic, assign) BOOL isPresenting; +@property(nonatomic, weak) QMUISheetRootContainerViewController *containerViewController; +@end + +@implementation QMUISheetRootControllerAnimator + +- (NSTimeInterval)transitionDuration:(id)transitionContext { + return .25;// 在 viewSafeAreaInsetsDidChange 里也有一个 duration,两者保持一致 +} + +- (void)animateTransition:(id)transitionContext { + + if (self.isPresenting) { + UIView *containerView = transitionContext.containerView; + UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];// 这个是 UINavigationController.view + + // 把 layout 独立一个方法,不直接调用 [self.view setNeedsLayout] 是因为后者的做法会影响业务界面生命周期方法的时序(具体参考上方 animateTransition 的注释) + // 此时 nav 里的导航栏等 subviews 已经布局好,但 containerRootVc 尚未被添加到 nav 里,所以它的 safeAreaInsets 不准确(为0),所以无法在此刻就计算出一个准确的浮层高度,所以通过标志位的方式延后到 viewSafeAreaInsetsDidChange 里处理 + self.containerViewController.shouldPerformPresentAnimation = YES; + [containerView addSubview:toView]; + toView.frame = containerView.bounds; + [transitionContext completeTransition:!transitionContext.transitionWasCancelled]; + return; + } + + [UIView qmui_animateWithAnimated:transitionContext.animated duration:[self transitionDuration:transitionContext] delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + self.containerViewController.dimmingControl.alpha = 0; + self.containerViewController.containerView.transform = CGAffineTransformMakeTranslation(0, CGRectGetHeight(self.containerViewController.containerView.frame)); + } completion:^(BOOL finished) { + [transitionContext completeTransition:!transitionContext.transitionWasCancelled]; + }]; +} + +@end + +@implementation QMUISheetRootContainerViewController + +- (instancetype)initWithRootViewController:(UIViewController *)rootViewController { + if (self = [self init]) { + _rootViewController = rootViewController; + [self addChildViewController:rootViewController]; + } + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + _dimmingControl = [[UIControl alloc] init]; + self.dimmingControl.backgroundColor = self.rootViewController.qmui_sheetPresentation.dimmingColor; + self.dimmingControl.alpha = 0; + [self.dimmingControl addTarget:self action:@selector(handleDimmingControlEvent) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.dimmingControl]; + + _containerView = [[UIView alloc] init]; + self.containerView.layer.cornerRadius = self.rootViewController.qmui_sheetPresentation.cornerRadius; + self.containerView.layer.maskedCorners = kCALayerMinXMinYCorner|kCALayerMaxXMinYCorner; + self.containerView.layer.cornerCurve = kCACornerCurveContinuous; + self.containerView.clipsToBounds = YES; + [self.view addSubview:self.containerView]; + + [self.containerView addSubview:self.rootViewController.view]; + + _navigationBar = [[QMUISheetPresentationNavigationBar alloc] init]; + self.navigationBar.hidden = !self.rootViewController.qmui_sheetPresentation.shouldShowNavigationBar; + self.navigationBar.navigationItem = self.rootViewController.navigationItem; + [self.containerView addSubview:self.navigationBar]; + + [self.rootViewController didMoveToParentViewController:self]; + + self.edgePan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleEdgePan:)]; + self.edgePan.edges = UIRectEdgeLeft; + self.edgePan.qmui_multipleDelegatesEnabled = YES; + self.edgePan.delegate = self; + [self.view addGestureRecognizer:self.edgePan]; + + self.pullPan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePullPan:)]; + self.pullPan.qmui_multipleDelegatesEnabled = YES; + self.pullPan.delegate = self; + [self.pullPan requireGestureRecognizerToFail:self.edgePan]; + [self.view addGestureRecognizer:self.pullPan]; +} + +- (UINavigationItem *)navigationItem { + return self.rootViewController.navigationItem; +} + +- (void)viewSafeAreaInsetsDidChange { + [super viewSafeAreaInsetsDidChange]; + if (!self.shouldPerformPresentAnimation) return; + + CGFloat bottom = self.view.safeAreaInsets.bottom; + if (IS_NOTCHED_SCREEN && bottom <= 0) return; + + self.dimmingControl.alpha = 0; + [self layout]; + self.containerView.transform = CGAffineTransformMakeTranslation(0, CGRectGetHeight(self.containerView.frame)); + [UIView animateWithDuration:.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + self.dimmingControl.alpha = 1; + self.containerView.transform = CGAffineTransformIdentity; + } completion:nil]; + + self.shouldPerformPresentAnimation = NO; +} + +// 把 layout 独立一个方法,不直接调用 [self.view setNeedsLayout] 是因为后者的做法会影响业务界面生命周期方法的时序(iOS 17 上验证,iOS 15 顺序一致,但两个 layout 方法会调用多两次)。 +// 如果普通 push,时序应该是 viewWillAppear:-viewIsAppearing:-viewWillLayoutSubviews-viewDidLayoutSubviews,而在 viewSafeAreaInsetsDidChange 里做动画前就调用 [self.view setNeedsLayout],时序会变成 viewWillLayoutSubviews-viewDidLayoutSubviews-viewWillAppear:-viewIsAppearing:,这令业务界面无法用一套代码同时兼容普通 push 模式和 sheet 模式。 +- (void)layout { + self.dimmingControl.frame = self.view.bounds; + + CGFloat navigationBarHeight = 0; + if (!self.navigationBar.hidden) { + [self.navigationBar sizeToFit]; + navigationBarHeight = CGRectGetHeight(self.navigationBar.frame); + } + CGFloat maximumWidth = MIN(QMUIHelper.screenSizeFor67InchAndiPhone14Later.width, CGRectGetWidth(self.view.bounds)); + CGFloat maximumHeight = CGRectGetHeight(self.view.bounds); + CGSize size = CGSizeZero; + if (self.rootViewController.qmui_sheetPresentation.preferredSheetContentSizeBlock) { + size = self.rootViewController.qmui_sheetPresentation.preferredSheetContentSizeBlock(self.rootViewController.qmui_sheetPresentation, CGSizeMake(MIN(maximumWidth, CGRectGetWidth(self.view.bounds)), maximumHeight)); + } else { + size = CGSizeMake(maximumWidth, 200);// 随便搞个默认值 + } + if (size.height != CGFLOAT_MAX && !isinf(size.height)) {// 如果业务传过来 CGFLOAT_MAX 则表示它希望撑满高度,此时就不要再进行叠加运算了,否则会因为溢出而产生错误的高度 + size.height = navigationBarHeight + size.height + self.view.safeAreaInsets.bottom; + } + CGSize containerSize = CGSizeMake(MIN(maximumWidth, size.width), MIN(maximumHeight, size.height)); + self.containerView.qmui_frameApplyTransform = CGRectMake(CGFloatGetCenter(CGRectGetWidth(self.view.bounds), containerSize.width), CGRectGetHeight(self.view.bounds) - containerSize.height, containerSize.width, containerSize.height); + if (!self.navigationBar.hidden) { + self.navigationBar.frame = CGRectMake(0, 0, CGRectGetWidth(self.containerView.bounds), navigationBarHeight); + [self.navigationBar setNeedsLayout]; + } + self.rootViewController.view.frame = CGRectMake(0, navigationBarHeight, CGRectGetWidth(self.containerView.bounds), CGRectGetHeight(self.containerView.bounds) - navigationBarHeight); +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + [self layout]; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + return self.rootViewController.supportedInterfaceOrientations; +} + +- (BOOL)prefersStatusBarHidden { + return self.rootViewController.prefersStatusBarHidden; +} + +- (UIViewController *)childViewControllerForStatusBarStyle { + return self.rootViewController; +} + +- (UIViewController *)childViewControllerForStatusBarHidden { + return self.rootViewController; +} + +- (UIViewController *)childViewControllerForHomeIndicatorAutoHidden { + return self.rootViewController; +} + +- (UIViewController *)qmui_visibleViewControllerIfExist { + return self.rootViewController; +} + +- (void)handleDimmingControlEvent { + if (!self.rootViewController.qmui_sheetPresentation.modal) { + [self dismissViewControllerAnimated:YES completion:nil]; + } +} + +- (void)handleEdgePan:(UIScreenEdgePanGestureRecognizer *)pan { + CGFloat process = [pan translationInView:pan.view].x / CGRectGetWidth(self.navigationController.view.bounds); + process = MIN(1.0, MAX(0.0, process)); + switch (pan.state) { + case UIGestureRecognizerStateBegan: + self.interactiveTransition = [[UIPercentDrivenInteractiveTransition alloc] init]; + [self dismissViewControllerAnimated:YES completion:nil]; + break; + case UIGestureRecognizerStateChanged: + [self.interactiveTransition updateInteractiveTransition:process]; + break; + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateCancelled: { + CGPoint velocity = [pan velocityInView:pan.view]; + BOOL shouldFinish = velocity.x >= 0 && ((velocity.x > 800 && process > 0.1) || (velocity.x <= 800 && process > 0.2)); + if (shouldFinish) { + [self.interactiveTransition finishInteractiveTransition]; + } else { + [self.interactiveTransition cancelInteractiveTransition]; + } + self.interactiveTransition = nil; + } + break; + default: + break; + } +} + +- (void)handlePullPan:(UIPanGestureRecognizer *)pan { + CGFloat process = [pan translationInView:pan.view].y / CGRectGetHeight(self.containerView.frame); + process = MIN(1.0, MAX(0.0, process)); + switch (pan.state) { + case UIGestureRecognizerStateBegan: + self.interactiveTransition = [[UIPercentDrivenInteractiveTransition alloc] init]; + [self dismissViewControllerAnimated:YES completion:nil]; + break; + case UIGestureRecognizerStateChanged: + [self.interactiveTransition updateInteractiveTransition:process]; + break; + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateCancelled: { + CGPoint velocity = [pan velocityInView:pan.view]; + BOOL shouldFinish = velocity.y >= 0 && ((velocity.y > 800 && process > 0.1) || (velocity.y <= 800 && process > 0.2)); + if (shouldFinish) { + [self.interactiveTransition finishInteractiveTransition]; + } else { + [self.interactiveTransition cancelInteractiveTransition]; + } + self.interactiveTransition = nil; + } + break; + default: + break; + } +} + +#pragma mark - + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer == self.edgePan && !self.rootViewController.qmui_sheetPresentation.supportsSwipeToDismiss) return NO; + if (gestureRecognizer == self.pullPan && !self.rootViewController.qmui_sheetPresentation.supportsPullToDismiss) return NO; + return YES; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { + if (gestureRecognizer != self.edgePan && gestureRecognizer != self.pullPan) { + return YES; + } + // navigationBar 上的按钮优先响应点击,不响应手势 + BOOL result = !([touch.view isDescendantOfView:self.navigationBar] && [touch.view isKindOfClass:UIControl.class]); + return result; +} + +#pragma mark - + +- (BOOL)preferredNavigationBarHidden { + return YES; +} + +- (BOOL)shouldCustomizeNavigationBarTransitionIfHideable { + return YES; +} + +#pragma mark - + +- (id)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { + QMUISheetRootControllerAnimator *animator = [[QMUISheetRootControllerAnimator alloc] init]; + animator.isPresenting = YES; + animator.containerViewController = self; + return animator; +} + +- (id)animationControllerForDismissedController:(UIViewController *)dismissed { + QMUISheetRootControllerAnimator *animator = [[QMUISheetRootControllerAnimator alloc] init]; + animator.containerViewController = self; + return animator; +} + +- (id)interactionControllerForDismissal:(id)animator { + return self.interactiveTransition; +} + +@end + + +@interface QMUISheetPresentation () + +/// 对应 UINavigationController.rootViewController,也即承载浮层的全屏容器 +@property(nonatomic, weak, nullable) QMUISheetRootContainerViewController *containerViewController; + +/// 对应浮层内正在展示的实际界面 +@property(nonatomic, weak, nullable) UIViewController *rootViewController; +@end + +@implementation QMUISheetPresentation + +- (instancetype)initWithContainerViewController:(QMUISheetRootContainerViewController *)containerViewController { + if (self = [super init]) { + _supportsSwipeToDismiss = YES; + _supportsPullToDismiss = YES; + _shouldShowNavigationBar = YES; + _dimmingColor = QMUICMIActivated ? UIColorMask : [UIColor.blackColor colorWithAlphaComponent:.35]; + _cornerRadius = 10; + + self.containerViewController = containerViewController; + self.rootViewController = self.containerViewController.rootViewController; + } + return self; +} + +- (void)setModal:(BOOL)modal { + _modal = modal; + + // 开启 modal 时关闭手势,业务可手动再打开 + if (modal) { + self.supportsSwipeToDismiss = NO; + self.supportsPullToDismiss = NO; + } +} + +- (void)setShouldShowNavigationBar:(BOOL)shouldShowNavigationBar { + _shouldShowNavigationBar = shouldShowNavigationBar; + self.containerViewController.navigationBar.hidden = !shouldShowNavigationBar; + [self.containerViewController.view setNeedsLayout]; +} + +- (void)setCornerRadius:(CGFloat)cornerRadius { + _cornerRadius = cornerRadius; + self.containerViewController.containerView.layer.cornerRadius = cornerRadius; +} + +@end + +@implementation UIViewController (QMUISheetSupports) + +- (BOOL)qmui_isPresentedInSheet { + return [self.parentViewController isKindOfClass:QMUISheetRootContainerViewController.class]; +} + +static char kAssociatedObjectKey_QMUISheetPresentation; +- (QMUISheetPresentation *)qmui_sheetPresentation { + QMUISheetPresentation *result = (QMUISheetPresentation *)objc_getAssociatedObject(self, &kAssociatedObjectKey_QMUISheetPresentation); + if (!result) { + result = [[QMUISheetPresentation alloc] initWithContainerViewController:nil]; + objc_setAssociatedObject(self, &kAssociatedObjectKey_QMUISheetPresentation, result, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return result; +} + +- (QMUISheetPresentationNavigationBar *)qmui_sheetPresentationNavigationBar { + return self.qmui_sheetPresentation.containerViewController.navigationBar; +} + +- (UIScreenEdgePanGestureRecognizer *)qmui_sheetPresentationSwipeGestureRecognizer { + return self.qmui_sheetPresentation.containerViewController.edgePan; +} + +- (UIPanGestureRecognizer *)qmui_sheetPresentationPullGestureRecognizer { + return self.qmui_sheetPresentation.containerViewController.pullPan; +} + +- (void)qmui_invalidateSheetPresentationLayout { + if (self.qmui_sheetPresentation.containerViewController.viewLoaded) { + [self.qmui_sheetPresentation.containerViewController.view setNeedsLayout]; + if (self.view.window) { + [UIView animateWithDuration:.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + [self.qmui_sheetPresentation.containerViewController.view layoutIfNeeded]; + } completion:nil]; + } + } +} + +@end + +@implementation QMUINavigationController (QMUISheetSupports) + +- (instancetype)qmui_initWithSheetRootViewController:(UIViewController *)rootViewController { + QMUISheetRootContainerViewController *container = [[QMUISheetRootContainerViewController alloc] initWithRootViewController:rootViewController]; + rootViewController.qmui_sheetPresentation.containerViewController = container; + rootViewController.qmui_sheetPresentation.rootViewController = rootViewController; + + __typeof(self)results = [self initWithRootViewController:container]; + results.modalPresentationStyle = UIModalPresentationCustom; + results.transitioningDelegate = container; + return results; +} + ++ (void)qmuiss_hookViewControllerIfNeeded { + // TODO: navigationItem 变化时更新 navigationBar +// [QMUIHelper executeBlock:^{ +// } oncePerIdentifier:@"QMUISheetPresentation"]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISlider.h b/QMUI/QMUIKit/QMUIComponents/QMUISlider.h deleted file mode 100644 index 9b8e9491..00000000 --- a/QMUI/QMUIKit/QMUIComponents/QMUISlider.h +++ /dev/null @@ -1,46 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUISlider.h -// qmui -// -// Created by QMUI Team on 2017/6/1. -// - -#import - -/** - * 相比系统的 UISlider,支持: - * 1. 修改背后导轨的高度 - * 2. 修改圆点的大小 - * 3. 修改圆点的阴影样式 - */ -@interface QMUISlider : UISlider - -/// 背后导轨的高度,默认为 0,表示使用系统默认的高度。 -@property(nonatomic, assign) IBInspectable CGFloat trackHeight UI_APPEARANCE_SELECTOR; - -/// 中间圆球的大小,默认为 CGSizeZero -/// @warning 注意若设置了 thumbSize 但没设置 thumbColor,则圆点的颜色会使用 self.tintColor 的颜色(但系统 UISlider 默认的圆点颜色是白色带阴影) -@property(nonatomic, assign) IBInspectable CGSize thumbSize UI_APPEARANCE_SELECTOR; - -/// 中间圆球的颜色,默认为 nil。 -/// @warning 注意请勿使用系统的 thumbTintColor,因为 thumbTintColor 和 thumbImage 是互斥的,设置一个会导致另一个被清空,从而导致样式错误。 -@property(nonatomic, strong) IBInspectable UIColor *thumbColor UI_APPEARANCE_SELECTOR; - -/// 中间圆球的阴影颜色,默认为 nil -@property(nonatomic, strong) IBInspectable UIColor *thumbShadowColor UI_APPEARANCE_SELECTOR; - -/// 中间圆球的阴影偏移值,默认为 CGSizeZero -@property(nonatomic, assign) IBInspectable CGSize thumbShadowOffset UI_APPEARANCE_SELECTOR; - -/// 中间圆球的阴影扩散度,默认为 0 -@property(nonatomic, assign) IBInspectable CGFloat thumbShadowRadius UI_APPEARANCE_SELECTOR; - -@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISlider.m b/QMUI/QMUIKit/QMUIComponents/QMUISlider.m deleted file mode 100644 index 55b48918..00000000 --- a/QMUI/QMUIKit/QMUIComponents/QMUISlider.m +++ /dev/null @@ -1,96 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUISlider.m -// qmui -// -// Created by QMUI Team on 2017/6/1. -// - -#import "QMUISlider.h" -#import "QMUICore.h" -#import "UIImage+QMUI.h" - -@implementation QMUISlider - -- (void)setThumbSize:(CGSize)thumbSize { - _thumbSize = thumbSize; - [self updateThumbImage]; -} - -- (void)setThumbColor:(UIColor *)thumbColor { - _thumbColor = thumbColor; - [self updateThumbImage]; -} - -- (void)updateThumbImage { - if (!CGSizeIsEmpty(self.thumbSize)) { - UIColor *thumbColor = self.thumbColor ?: self.tintColor; - UIImage *thumbImage = [UIImage qmui_imageWithShape:QMUIImageShapeOval size:_thumbSize tintColor:thumbColor]; - [self setThumbImage:thumbImage forState:UIControlStateNormal]; - [self setThumbImage:thumbImage forState:UIControlStateHighlighted]; - } -} - -- (void)setThumbShadowColor:(UIColor *)thumbShadowColor { - _thumbShadowColor = thumbShadowColor; - UIView *thumbView = [self thumbViewIfExist]; - if (thumbView) { - thumbView.layer.shadowColor = _thumbShadowColor.CGColor; - thumbView.layer.shadowOpacity = _thumbShadowColor ? 1 : 0; - } -} - -- (void)setThumbShadowOffset:(CGSize)thumbShadowOffset { - _thumbShadowOffset = thumbShadowOffset; - UIView *thumbView = [self thumbViewIfExist]; - if (thumbView) { - thumbView.layer.shadowOffset = _thumbShadowOffset; - } -} - -- (void)setThumbShadowRadius:(CGFloat)thumbShadowRadius { - _thumbShadowRadius = thumbShadowRadius; - UIView *thumbView = [self thumbViewIfExist]; - if (thumbView) { - thumbView.layer.shadowRadius = thumbShadowRadius; - } -} - -- (UIView *)thumbViewIfExist { - // thumbView 并非在一开始就存在,而是在某个时机才生成的,所以可能返回 nil - UIView *thumbView = [self valueForKey:@"thumbView"]; - return thumbView; -} - -#pragma mark - Override - -- (CGRect)trackRectForBounds:(CGRect)bounds { - CGRect result = [super trackRectForBounds:bounds]; - if (self.trackHeight == 0) { - return result; - } - - result = CGRectSetHeight(result, self.trackHeight); - result = CGRectSetY(result, CGFloatGetCenter(CGRectGetHeight(bounds), CGRectGetHeight(result))); - return result; -} - -- (void)didAddSubview:(UIView *)subview { - [super didAddSubview:subview]; - if (subview && subview == [self thumbViewIfExist]) { - UIView *thumbView = subview; - thumbView.layer.shadowColor = self.thumbShadowColor.CGColor; - thumbView.layer.shadowOpacity = self.thumbShadowColor ? 1 : 0; - thumbView.layer.shadowOffset = self.thumbShadowOffset; - thumbView.layer.shadowRadius = self.thumbShadowRadius; - } -} - -@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITableView.h b/QMUI/QMUIKit/QMUIComponents/QMUITableView.h index 38f28640..4cfc9383 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITableView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUITableView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITableView.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITableView.m b/QMUI/QMUIKit/QMUIComponents/QMUITableView.m index 54acd684..f65a5c16 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITableView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUITableView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITableView.m @@ -41,8 +41,8 @@ - (void)didInitialize { } - (void)dealloc { - self.delegate = nil; self.dataSource = nil; + self.delegate = nil; } // 保证一直存在tableFooterView,以去掉列表内容不满一屏时尾部的空白分割线 diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITableViewCell.h b/QMUI/QMUIKit/QMUIComponents/QMUITableViewCell.h index 91c49fb1..c5582d56 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITableViewCell.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUITableViewCell.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITableViewCell.h @@ -16,43 +16,52 @@ #import #import "UITableView+QMUI.h" +NS_ASSUME_NONNULL_BEGIN @interface QMUITableViewCell : UITableViewCell @property(nonatomic, assign, readonly) UITableViewCellStyle style; /** - * imageEdgeInsets,这个属性用来调整imageView里面图片的位置,有些情况titleLabel前面是一个icon,但是icon与titleLabel的间距不是你想要的。
- * @warning 目前只对UITableViewCellStyleDefault和UITableViewCellStyleSubtitle类型的cell开放 + * 调整 imageView 的位置偏移,常用于调整 imageView 和 textLabel 之间的间距,默认为 UIEdgeInsetsZero。 + * @warning 目前只对 UITableViewCellStyleDefault 和 UITableViewCellStyleSubtitle 类型的 cell 开放 */ @property(nonatomic, assign) UIEdgeInsets imageEdgeInsets; /** - * textLabelEdgeInsets,这个属性和imageEdgeInsets合作使用,用来调整titleLabel的位置,默认为 UIEdgeInsetsZero。
- * @warning 目前只对UITableViewCellStyleDefault和UITableViewCellStyleSubtitle类型的cell开放。 + * 调整 textLabel 的位置偏移,默认为 UIEdgeInsetsZero。 + * @warning 目前只对 UITableViewCellStyleDefault 和 UITableViewCellStyleSubtitle 类型的 cell 开放 */ @property(nonatomic, assign) UIEdgeInsets textLabelEdgeInsets; -/// 与textLabelEdgeInsets一致,作用目标为detailTextLabel,默认为 UIEdgeInsetsZero。 +/// 调整 detailTextLabel 的位置偏移,默认为 UIEdgeInsetsZero。 @property(nonatomic, assign) UIEdgeInsets detailTextLabelEdgeInsets; -/// 用于调整右边 accessoryView 的布局偏移,默认为 UIEdgeInsetsZero。 +/** + 调整右边 accessoryView 的布局偏移,默认为 UIEdgeInsetsZero。 + @warning 对系统原生的 view 不生效(例如向右箭头、“i”详情按钮等),如果通过配置表设置了 TableViewCellDisclosureIndicatorImage,由于该配置本质上是使用了自定义的 accessoryView 来实现,所以这个属性对其生效。 + */ @property(nonatomic, assign) UIEdgeInsets accessoryEdgeInsets; -/// 用于调整accessoryView的点击响应区域,可用负值扩大点击范围,默认为(-12, -12, -12, -12) +/** + 调整右边 accessoryView 的点击响应区域,可用负值扩大点击范围,默认为(-12, -12, -12, -12)。 + @warning 对系统原生的 view 不生效(例如向右箭头、“i”详情按钮等),如果通过配置表设置了 TableViewCellDetailButtonImage,由于该配置本质上是使用了自定义的 accessoryView 来实现,所以这个属性对其生效。 + */ @property(nonatomic, assign) UIEdgeInsets accessoryHitTestEdgeInsets; -/// 设置当前cell是否enabled,setter方法里面会修改当前的subviews样式。 +/// 设置当前 cell 是否可用,setter 方法里面会修改当前的 subviews 样式,以展示出禁用的样式,具体样式请查看源码。 @property(nonatomic, assign, getter = isEnabled) BOOL enabled; -/// 保存对tableView的弱引用,在布局时可能会使用到tableView的一些属性例如separatorColor等。只有使用下面两个 initForTableView: 的接口初始化时这个属性才有值,否则就只能自己初始化后赋值 -@property(nonatomic, weak) UITableView *parentTableView; +/// 保存对 tableView 的弱引用,在布局时可能会使用到 tableView 的一些属性例如 separatorColor 等 +@property(nonatomic, weak, nullable) __kindof UITableView *parentTableView; /** * cell 处于 section 中的位置,要求: * 1. cell 使用 initForTableViewXxx 方法初始化,或者初始化完后为 parentTableView 属性赋值。 * 2. 在 cellForRow 里调用 [cell updateCellAppearanceWithIndexPath:] 方法。 * 3. 之后即可通过 cellPosition 获取到正确的位置。 + * + * @note UITableViewCell(QMUI) 内也有一个 qmui_cellPosition,那个需要在 willDisplayCell 后才有值,cellForRow 里是用不了的,所以 QMUITableViewCell 才增加 cellPosition。 */ @property(nonatomic, assign, readonly) QMUITableViewCellPosition cellPosition; @@ -65,10 +74,10 @@ * * @return 一个QMUITableViewCell实例 */ -- (instancetype)initForTableView:(UITableView *)tableView withStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier; +- (nullable instancetype)initForTableView:(nullable UITableView *)tableView withStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier; /// 同上 -- (instancetype)initForTableView:(UITableView *)tableView withReuseIdentifier:(NSString *)reuseIdentifier; +- (nullable instancetype)initForTableView:(nullable UITableView *)tableView withReuseIdentifier:(NSString *)reuseIdentifier; @end @@ -81,6 +90,8 @@ - (void)didInitializeWithStyle:(UITableViewCellStyle)style NS_REQUIRES_SUPER; /// 用于继承的接口,设置一些cell相关的UI,需要自 cellForRowAtIndexPath 里面调用。默认实现是设置当前cell在哪个position。 -- (void)updateCellAppearanceWithIndexPath:(NSIndexPath *)indexPath NS_REQUIRES_SUPER; +- (void)updateCellAppearanceWithIndexPath:(nullable NSIndexPath *)indexPath NS_REQUIRES_SUPER; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITableViewCell.m b/QMUI/QMUIKit/QMUIComponents/QMUITableViewCell.m index 1e13728b..ff908098 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITableViewCell.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUITableViewCell.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITableViewCell.m @@ -17,9 +17,11 @@ #import "QMUICore.h" #import "QMUIButton.h" #import "UITableView+QMUI.h" +#import "UITableViewCell+QMUI.h" @interface QMUITableViewCell() +@property(nonatomic, assign) BOOL initByTableView; @property(nonatomic, assign, readwrite) QMUITableViewCellPosition cellPosition; @property(nonatomic, assign, readwrite) UITableViewCellStyle style; @property(nonatomic, strong) UIImageView *defaultAccessoryImageView; @@ -31,14 +33,18 @@ @implementation QMUITableViewCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { - [self didInitializeWithStyle:style]; + if (!self.initByTableView) { + [self didInitializeWithStyle:style]; + } } return self; } - (instancetype)initForTableView:(UITableView *)tableView withStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { - if (self = [self initWithStyle:style reuseIdentifier:reuseIdentifier]) { + self.initByTableView = YES; + if (self = [self initWithStyle:style reuseIdentifier:reuseIdentifier]) {// 这里需要调用 self 的 initWithStyle,而不是 super,目的是为了让业务在重写 init 方法时可以沿用系统默认的思路,去重写 initWithStyle:reuseIdentifier:,但在 vc 里使用 cell 时又可以直接调用 initForTableView:withStyle:。 self.parentTableView = tableView; + [self didInitializeWithStyle:style];// 因为设置了 parentTableView,样式可能都需要变,所以这里重新执行一次 didInitializeWithStyle: 里的 qmui_styledAsQMUITableViewCell } return self; } @@ -54,15 +60,7 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { return self; } -- (void)dealloc { - self.parentTableView = nil; -} - -// 解决 iOS 8 以后的 cell 中 separatorInset 受 layoutMargins 影响的问题 -- (UIEdgeInsets)layoutMargins { - return UIEdgeInsetsZero; -} - +// layoutSubviews 里不可以拿 textLabel 的 minX 来设置 separatorInset,如果要设置只能写死一个值,否则会导致 textLabel 的 minX 逐渐叠加从而使 textLabel 被移出屏幕外 - (void)layoutSubviews { [super layoutSubviews]; @@ -104,7 +102,7 @@ - (void)layoutSubviews { } } if (hasCustomTextLabelEdgeInsets) { - textLabelFrame.origin.x += self.textLabelEdgeInsets.left - self.imageEdgeInsets.right; + textLabelFrame.origin.x += self.textLabelEdgeInsets.left - self.textLabelEdgeInsets.right; textLabelFrame.origin.y += self.textLabelEdgeInsets.top - self.textLabelEdgeInsets.bottom; textLabelFrame.size.width = fmin(CGRectGetWidth(textLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(textLabelFrame)); } @@ -117,9 +115,6 @@ - (void)layoutSubviews { self.imageView.frame = imageViewFrame; self.textLabel.frame = textLabelFrame; self.detailTextLabel.frame = detailTextLabelFrame; - - // `layoutSubviews`这里不可以拿textLabel的minX来设置separatorInset,如果要设置只能写死一个值 - // 否则会导致textLabel的minX逐渐叠加从而使textLabel被移出屏幕外 } // 由于调整 accessoryEdgeInsets 可能会影响 contentView 的宽度,所以几个 subviews 的布局也要保护一下 @@ -133,22 +128,20 @@ - (void)layoutSubviews { } } -- (void)setBackgroundColor:(UIColor *)backgroundColor { - [super setBackgroundColor:backgroundColor]; - if (self.backgroundView) { - self.backgroundView.backgroundColor = backgroundColor; - } +// QMUITableViewCell 由于 init 时就把 tableView 传进来,所以可以在更早的时机拿到 qmui_tableView 的值,如果是系统的 UITableView,默认只能在添加到 tableView 上之后才可以获取到引用 +- (UITableView *)qmui_tableView { + return self.parentTableView ?: [super qmui_tableView]; } - (void)setEnabled:(BOOL)enabled { if (_enabled != enabled) { if (enabled) { self.userInteractionEnabled = YES; - UIColor *titleLabelColor = TableViewCellTitleLabelColor; + UIColor *titleLabelColor = self.qmui_styledTextLabelColor; if (titleLabelColor) { self.textLabel.textColor = titleLabelColor; } - UIColor *detailLabelColor = TableViewCellDetailLabelColor; + UIColor *detailLabelColor = self.qmui_styledDetailTextLabelColor; if (detailLabelColor) { self.detailTextLabel.textColor = detailLabelColor; } @@ -175,6 +168,7 @@ - (void)initDefaultAccessoryButtonIfNeeded { if (!self.defaultAccessoryButton) { self.defaultAccessoryButton = [[QMUIButton alloc] init]; [self.defaultAccessoryButton addTarget:self action:@selector(handleAccessoryButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + self.defaultAccessoryButton.accessibilityLabel = @"更多信息"; } } @@ -226,7 +220,7 @@ - (void)setAccessoryType:(UITableViewCellAccessoryType)accessoryType { UIImage *indicatorImage = TableViewCellDisclosureIndicatorImage; if (detailButtonImage) { - NSAssert(!!indicatorImage, @"TableViewCellDetailButtonImage 和 TableViewCellDisclosureIndicatorImage 必须同时使用,但目前后者为 nil"); + QMUIAssert(!!indicatorImage, NSStringFromClass(self.class), @"TableViewCellDetailButtonImage 和 TableViewCellDisclosureIndicatorImage 必须同时使用,但目前后者为 nil"); [self initDefaultDetailDisclosureViewIfNeeded]; [self initDefaultAccessoryButtonIfNeeded]; [self.defaultAccessoryButton setImage:detailButtonImage forState:UIControlStateNormal]; @@ -238,7 +232,7 @@ - (void)setAccessoryType:(UITableViewCellAccessoryType)accessoryType { } if (indicatorImage) { - NSAssert(!!detailButtonImage, @"TableViewCellDetailButtonImage 和 TableViewCellDisclosureIndicatorImage 必须同时使用,但目前前者为 nil"); + QMUIAssert(!!detailButtonImage, NSStringFromClass(self.class), @"TableViewCellDetailButtonImage 和 TableViewCellDisclosureIndicatorImage 必须同时使用,但目前前者为 nil"); [self initDefaultDetailDisclosureViewIfNeeded]; [self initDefaultAccessoryImageViewIfNeeded]; self.defaultAccessoryImageView.image = indicatorImage; @@ -303,8 +297,8 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { } - (void)handleAccessoryButtonEvent:(QMUIButton *)detailButton { - if ([self.parentTableView.delegate respondsToSelector:@selector(tableView:accessoryButtonTappedForRowWithIndexPath:)]) { - [self.parentTableView.delegate tableView:self.parentTableView accessoryButtonTappedForRowWithIndexPath:[self.parentTableView qmui_indexPathForRowAtView:detailButton]]; + if ([self.qmui_tableView.delegate respondsToSelector:@selector(tableView:accessoryButtonTappedForRowWithIndexPath:)]) { + [self.qmui_tableView.delegate tableView:self.qmui_tableView accessoryButtonTappedForRowWithIndexPath:[self.qmui_tableView qmui_indexPathForRowAtView:detailButton]]; } } @@ -313,49 +307,27 @@ - (void)handleAccessoryButtonEvent:(QMUIButton *)detailButton { @implementation QMUITableViewCell(QMUISubclassingHooks) - (void)didInitializeWithStyle:(UITableViewCellStyle)style { + self.initByTableView = NO; _cellPosition = QMUITableViewCellPositionNone; _style = style; _enabled = YES; _accessoryHitTestEdgeInsets = UIEdgeInsetsMake(-12, -12, -12, -12); - self.textLabel.font = UIFontMake(16); - self.textLabel.backgroundColor = UIColorClear; - UIColor *titleLabelColor = TableViewCellTitleLabelColor; - if (titleLabelColor) { - self.textLabel.textColor = titleLabelColor; - } - - self.detailTextLabel.font = UIFontMake(15); - self.detailTextLabel.backgroundColor = UIColorClear; - UIColor *detailLabelColor = TableViewCellDetailLabelColor; - if (detailLabelColor) { - self.detailTextLabel.textColor = detailLabelColor; - } - - UIColor *backgroundColor = TableViewCellBackgroundColor; - if (backgroundColor) { - self.backgroundColor = backgroundColor; - } - - UIColor *selectedBackgroundColor = TableViewCellSelectedBackgroundColor; - if (selectedBackgroundColor) { - UIView *selectedBackgroundView = [[UIView alloc] init]; - selectedBackgroundView.backgroundColor = selectedBackgroundColor; - self.selectedBackgroundView = selectedBackgroundView; - } - + // TODO: molice 测一下时至今日还需要吗? // 因为在hitTest里扩大了accessoryView的响应范围,因此提高了系统一个与此相关的bug的出现几率,所以又在scrollView.delegate里做一些补丁性质的东西来修复 if ([self.subviews.firstObject isKindOfClass:[UIScrollView class]]) { UIScrollView *scrollView = (UIScrollView *)[self.subviews objectAtIndex:0]; scrollView.delegate = self; } + + [self qmui_styledAsQMUITableViewCell]; } - (void)updateCellAppearanceWithIndexPath:(NSIndexPath *)indexPath { // 子类继承 - if (indexPath && self.parentTableView) { - QMUITableViewCellPosition position = [self.parentTableView qmui_positionForRowAtIndexPath:indexPath]; + if (indexPath && self.qmui_tableView) { + QMUITableViewCellPosition position = [self.qmui_tableView qmui_positionForRowAtIndexPath:indexPath]; self.cellPosition = position; } else { self.cellPosition = QMUITableViewCellPositionNone; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.h b/QMUI/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.h index d188a0e7..5dde954a 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITableViewHeaderFooterView.h @@ -30,8 +30,8 @@ typedef NS_ENUM(NSUInteger, QMUITableViewHeaderFooterViewType) { * * 使用方式: * 基本与系统的 UITableViewHeaderFooterView 使用方式一致,额外需要做的事情有: - * 1. 如果要支持高度自动根据内容变化,则需要重写 tableView:heightForHeaderInSection:、tableView:heightForFooterInSection:,在里面调用 headerFooterView 的 sizeThatFits:。 - * 2. 如果要应用配置表样式,则设置 parentTableView 和 type 这两个属性即可。 + * 1. 如果要支持高度自动根据内容变化,则按系统的 self-sizing 方式,用 UITableViewAutomaticDimension 指定。或者重写 tableView:heightForHeaderInSection:、tableView:heightForFooterInSection:,在里面调用 headerFooterView 的 sizeThatFits:。 + * 2. 如果要应用配置表样式,则设置 parentTableView 和 type 这两个属性即可。特别的,QMUICommonTableViewController 里默认已经处理好 parentTableView 和 type,子类无需操作。 */ @interface QMUITableViewHeaderFooterView : UITableViewHeaderFooterView @@ -39,7 +39,7 @@ typedef NS_ENUM(NSUInteger, QMUITableViewHeaderFooterViewType) { @property(nonatomic, assign) QMUITableViewHeaderFooterViewType type; @property(nonatomic, strong, readonly) UILabel *titleLabel; -@property(nonatomic, strong) UIView *accessoryView; +@property(nonatomic, strong) __kindof UIView *accessoryView; @property(nonatomic, assign) UIEdgeInsets contentEdgeInsets; @property(nonatomic, assign) UIEdgeInsets accessoryViewMargins; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.m b/QMUI/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.m index 7b79699f..d0661f25 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITableViewHeaderFooterView.m @@ -16,41 +16,64 @@ #import "QMUITableViewHeaderFooterView.h" #import "QMUICore.h" #import "UIView+QMUI.h" +#import "UITableView+QMUI.h" +#import "UITableViewHeaderFooterView+QMUI.h" @implementation QMUITableViewHeaderFooterView - (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier { if (self = [super initWithReuseIdentifier:reuseIdentifier]) { - _titleLabel = [[UILabel alloc] init]; - self.titleLabel.numberOfLines = 0; - [self.contentView addSubview:self.titleLabel]; - - // remove system subviews - self.textLabel.hidden = YES; - self.detailTextLabel.hidden = YES; - self.backgroundView = [[UIView alloc] init];// 去掉默认的背景,以使 self.backgroundColor 生效 + [self didInitialize]; } return self; } +- (instancetype)initWithCoder:(NSCoder *)coder { + if (self = [super initWithCoder:coder]) { + [self didInitialize]; + } + return self; +} + +- (void)didInitialize { + _titleLabel = [[UILabel alloc] init]; + self.titleLabel.numberOfLines = 0; + [self.contentView addSubview:self.titleLabel]; + + // remove system subviews + self.textLabel.hidden = YES; + self.detailTextLabel.hidden = YES; + self.backgroundView = [[UIView alloc] init];// 去掉默认的背景,以便屏蔽系统对背景色的控制 +} + +// 系统的 UITableViewHeaderFooterView 不允许修改 backgroundColor,都应该放到 backgroundView 里,但却没有在文档中写明,只有不小心误用时才会在 Xcode 控制台里提示,所以这里做个转换,保护误用的情况。 +- (void)setBackgroundColor:(UIColor *)backgroundColor { +// [super setBackgroundColor:backgroundColor]; + self.backgroundView.backgroundColor = backgroundColor; +} + +- (UIColor *)backgroundColor { +// return [super backgroundColor]; + return self.backgroundView.backgroundColor; +} + - (void)updateAppearance { - if (!self.parentTableView) return; - if (self.type == QMUITableViewHeaderFooterViewTypeUnknow) return; + if (!QMUICMIActivated || (!self.parentTableView && !self.qmui_tableView) || self.type == QMUITableViewHeaderFooterViewTypeUnknow) return; - BOOL isPlainStyleTableView = self.parentTableView.style == UITableViewStylePlain; + UITableViewStyle style = (self.parentTableView ?: self.qmui_tableView).style; if (self.type == QMUITableViewHeaderFooterViewTypeHeader) { - self.titleLabel.font = isPlainStyleTableView ? TableViewSectionHeaderFont : TableViewGroupedSectionHeaderFont; - self.titleLabel.textColor = isPlainStyleTableView ? TableViewSectionHeaderTextColor : TableViewGroupedSectionHeaderTextColor; - self.contentEdgeInsets = isPlainStyleTableView ? TableViewSectionHeaderContentInset : TableViewGroupedSectionHeaderContentInset; - self.accessoryViewMargins = isPlainStyleTableView ? TableViewSectionHeaderAccessoryMargins : TableViewGroupedSectionHeaderAccessoryMargins; - self.backgroundView.backgroundColor = isPlainStyleTableView ? TableViewSectionHeaderBackgroundColor : UIColorClear; + self.titleLabel.font = PreferredValueForTableViewStyle(style, TableViewSectionHeaderFont, TableViewGroupedSectionHeaderFont, TableViewInsetGroupedSectionHeaderFont); + self.titleLabel.textColor = PreferredValueForTableViewStyle(style, TableViewSectionHeaderTextColor, TableViewGroupedSectionHeaderTextColor, TableViewInsetGroupedSectionHeaderTextColor); + self.contentEdgeInsets = PreferredValueForTableViewStyle(style, TableViewSectionHeaderContentInset, TableViewGroupedSectionHeaderContentInset, TableViewInsetGroupedSectionHeaderContentInset); + self.accessoryViewMargins = PreferredValueForTableViewStyle(style, TableViewSectionHeaderAccessoryMargins, TableViewGroupedSectionHeaderAccessoryMargins, TableViewInsetGroupedSectionHeaderAccessoryMargins); + self.backgroundView.backgroundColor = PreferredValueForTableViewStyle(style, TableViewSectionHeaderBackgroundColor, UIColorClear, UIColorClear); } else { - self.titleLabel.font = isPlainStyleTableView ? TableViewSectionFooterFont : TableViewGroupedSectionFooterFont; - self.titleLabel.textColor = isPlainStyleTableView ? TableViewSectionFooterTextColor : TableViewGroupedSectionFooterTextColor; - self.contentEdgeInsets = isPlainStyleTableView ? TableViewSectionFooterContentInset : TableViewGroupedSectionFooterContentInset; - self.accessoryViewMargins = isPlainStyleTableView ? TableViewSectionFooterAccessoryMargins : TableViewGroupedSectionFooterAccessoryMargins; - self.backgroundView.backgroundColor = isPlainStyleTableView ? TableViewSectionFooterBackgroundColor : UIColorClear; + self.titleLabel.font = PreferredValueForTableViewStyle(style, TableViewSectionFooterFont, TableViewGroupedSectionFooterFont, TableViewInsetGroupedSectionFooterFont); + self.titleLabel.textColor = PreferredValueForTableViewStyle(style, TableViewSectionFooterTextColor, TableViewGroupedSectionFooterTextColor, TableViewInsetGroupedSectionFooterTextColor); + self.contentEdgeInsets = PreferredValueForTableViewStyle(style, TableViewSectionFooterContentInset, TableViewGroupedSectionFooterContentInset, TableViewInsetGroupedSectionFooterContentInset); + self.accessoryViewMargins = PreferredValueForTableViewStyle(style, TableViewSectionFooterAccessoryMargins, TableViewGroupedSectionFooterAccessoryMargins, TableViewInsetGroupedSectionFooterAccessoryMargins); + self.backgroundView.backgroundColor = PreferredValueForTableViewStyle(style, TableViewSectionFooterBackgroundColor, UIColorClear, UIColorClear); } } @@ -62,10 +85,11 @@ - (void)layoutSubviews { self.accessoryView.qmui_top = self.contentEdgeInsets.top + CGFloatGetCenter(self.contentView.qmui_height - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets), self.accessoryView.qmui_height) + self.accessoryViewMargins.top - self.accessoryViewMargins.bottom; } - [self.titleLabel sizeToFit]; self.titleLabel.qmui_left = self.contentEdgeInsets.left; self.titleLabel.qmui_extendToRight = self.accessoryView ? self.accessoryView.qmui_left - self.accessoryViewMargins.left : self.contentView.qmui_width - self.contentEdgeInsets.right; - self.titleLabel.qmui_top = self.contentEdgeInsets.top + CGFloatGetCenter(self.contentView.qmui_height - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets), self.titleLabel.qmui_height); + CGSize titleLabelSize = [self.titleLabel sizeThatFits:CGSizeMake(self.titleLabel.qmui_width, CGFLOAT_MAX)]; + self.titleLabel.qmui_top = self.contentEdgeInsets.top + CGFloatGetCenter(self.contentView.qmui_height - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets), titleLabelSize.height); + self.titleLabel.qmui_height = titleLabelSize.height; } - (CGSize)sizeThatFits:(CGSize)size { @@ -91,6 +115,8 @@ - (void)setAccessoryView:(UIView *)accessoryView { [_accessoryView removeFromSuperview]; } _accessoryView = accessoryView; + self.isAccessibilityElement = NO; + self.titleLabel.accessibilityTraits |= UIAccessibilityTraitHeader; [self.contentView addSubview:accessoryView]; } diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITableViewProtocols.h b/QMUI/QMUIKit/QMUIComponents/QMUITableViewProtocols.h index 6d712bc1..b56fb2f7 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITableViewProtocols.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUITableViewProtocols.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITableViewProtocols.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITestView.h b/QMUI/QMUIKit/QMUIComponents/QMUITestView.h index 2e622095..d584b718 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITestView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUITestView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITestView.h diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITestView.m b/QMUI/QMUIKit/QMUIComponents/QMUITestView.m index e1b0664d..393d281a 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITestView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUITestView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITestView.m @@ -25,6 +25,19 @@ - (instancetype)init { return self; } +- (void)tintColorDidChange { + [super tintColorDidChange]; +} + +- (void)setTintColor:(UIColor *)tintColor { + [super setTintColor:tintColor]; + NSLog(@"QMUITestView setTintColor"); +} + +//- (void)setBackgroundColor:(UIColor *)backgroundColor { +// [super setBackgroundColor:backgroundColor]; +//} + - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { @@ -47,37 +60,37 @@ - (void)setFrame:(CGRect)frame { - (void)layoutSubviews { [super layoutSubviews]; - QMUILog(NSStringFromClass(self.class), @"%s, frame = %@", __func__, NSStringFromCGRect(self.frame)); + QMUILog(NSStringFromClass(self.class), @"frame = %@", NSStringFromCGRect(self.frame)); } - (void)willMoveToSuperview:(UIView *)newSuperview { [super willMoveToSuperview:newSuperview]; - QMUILog(NSStringFromClass(self.class), @"%s, superview is %@, newSuperview is %@, window is %@", __func__, self.superview, newSuperview, self.window); + QMUILog(NSStringFromClass(self.class), @"superview is %@, newSuperview is %@, window is %@", self.superview, newSuperview, self.window); } - (void)didMoveToSuperview { [super didMoveToSuperview]; - QMUILog(NSStringFromClass(self.class), @"%s, superview is %@, window is %@", __func__, self.superview, self.window); + QMUILog(NSStringFromClass(self.class), @"superview is %@, window is %@", self.superview, self.window); } - (void)willMoveToWindow:(UIWindow *)newWindow { [super willMoveToWindow:newWindow]; - QMUILog(NSStringFromClass(self.class), @"%s, self.window is %@, newWindow is %@", __func__, self.window, newWindow); + QMUILog(NSStringFromClass(self.class), @"self.window is %@, newWindow is %@", self.window, newWindow); } - (void)didMoveToWindow { [super didMoveToWindow]; - QMUILog(NSStringFromClass(self.class), @"%s, self.window is %@", __func__, self.window); + QMUILog(NSStringFromClass(self.class), @"self.window is %@", self.window); } - (void)addSubview:(UIView *)view { [super addSubview:view]; - QMUILog(NSStringFromClass(self.class), @"%s, subview is %@, subviews.count before addSubview is %@", __func__, view, @(self.subviews.count)); + QMUILog(NSStringFromClass(self.class), @"subview is %@, subviews.count before addSubview is %@", view, @(self.subviews.count)); } - (void)setHidden:(BOOL)hidden { [super setHidden:hidden]; - QMUILog(NSStringFromClass(self.class), @"%s, hidden is %@", __func__, @(hidden)); + QMUILog(NSStringFromClass(self.class), @"hidden is %@", @(hidden)); } - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITextField.h b/QMUI/QMUIKit/QMUIComponents/QMUITextField.h index 85267c9c..5b71e029 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITextField.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUITextField.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITextField.h @@ -21,6 +21,12 @@ @optional +/** + 由于 maximumTextLength 的实现方式导致业务无法再重写自己的 shouldChangeCharacters,否则会丢失 maximumTextLength 的功能。所以这里提供一个额外的 delegate,在 QMUI 内部逻辑返回 YES 的时候会再询问一次这个 delegate,从而给业务提供一个机会去限制自己的输入内容。如果 QMUI 内部逻辑本身就返回 NO(例如超过了 maximumTextLength 的长度),则不会触发这个方法。 + 当输入被这个方法拦截时,由于拦截逻辑是业务自己写的,业务能轻松获取到这个拦截的时机,所以此时不会调用 textField:didPreventTextChangeInRange:replacementString:。如果有类似 tips 之类的操作,可以直接在 return NO 之前处理。 + */ +- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string originalValue:(BOOL)originalValue; + /** * 配合 `maximumTextLength` 属性使用,在输入文字超过限制时被调用。 * @warning 在 UIControlEventEditingChanged 里也会触发文字长度拦截,由于此时 textField 的文字已经改变完,所以无法得知发生改变的文本位置及改变的文本内容,所以此时 range 和 replacementString 这两个参数的值也会比较特殊,具体请看参数讲解。 @@ -80,4 +86,19 @@ */ @property(nonatomic, assign) IBInspectable BOOL shouldCountingNonASCIICharacterAsTwo; +/** + * 控制输入框是否要出现“粘贴”menu + * @param sender 触发这次询问事件的来源 + * @param superReturnValue [super canPerformAction:withSender:] 的返回值,当你不需要控制这个 block 的返回值时,可以返回 superReturnValue + * @return 控制是否要出现“粘贴”menu,YES 表示出现,NO 表示不出现。当你想要返回系统默认的结果时,请返回参数 superReturnValue + */ +@property(nonatomic, copy) BOOL (^canPerformPasteActionBlock)(id sender, BOOL superReturnValue); + +/** + * 当输入框的“粘贴”事件被触发时,可通过这个 block 去接管事件的响应。 + * @param sender “粘贴”事件触发的来源,例如可能是一个 UIMenuController + * @return 返回值用于控制是否要调用系统默认的 paste: 实现,YES 表示执行完 block 后继续调用系统默认实现,NO 表示执行完 block 后就结束了,不调用 super。 + */ +@property(nonatomic, copy) BOOL (^pasteBlock)(id sender); + @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITextField.m b/QMUI/QMUIKit/QMUIComponents/QMUITextField.m index e2fc0f5f..44ce23f9 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITextField.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUITextField.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITextField.m @@ -39,7 +39,14 @@ - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self didInitialize]; - self.tintColor = TextFieldTintColor; + if (QMUICMIActivated) { + UIColor *textColor = TextFieldTextColor; + if (textColor) { + self.textColor = textColor; + } + + self.tintColor = TextFieldTintColor; + } } return self; } @@ -58,10 +65,13 @@ - (void)didInitialize { self.delegate = self.delegator; [self addTarget:self.delegator action:@selector(handleTextChangeEvent:) forControlEvents:UIControlEventEditingChanged]; - self.placeholderColor = UIColorPlaceholder; - self.textInsets = TextFieldTextInsets; self.shouldResponseToProgrammaticallyTextChanges = YES; self.maximumTextLength = NSUIntegerMax; + + if (QMUICMIActivated) { + self.placeholderColor = UIColorPlaceholder; + self.textInsets = TextFieldTextInsets; + } } - (void)dealloc { @@ -92,19 +102,17 @@ - (void)layoutSubviews { [super layoutSubviews]; // 以下代码修复系统的 UITextField 在 iOS 10 下的 bug:https://github.com/Tencent/QMUI_iOS/issues/64 - if (@available(iOS 10.0, *)) { - UIScrollView *scrollView = self.subviews.firstObject; - if (![scrollView isKindOfClass:[UIScrollView class]]) { - return; - } - - // 默认 delegate 是为 nil 的,所以我们才利用 delegate 修复这 个 bug,如果哪一天 delegate 不为 nil,就先不处理了。 - if (scrollView.delegate) { - return; - } - - scrollView.delegate = self.delegator; + UIScrollView *scrollView = self.subviews.firstObject; + if (![scrollView isKindOfClass:[UIScrollView class]]) { + return; } + + // 默认 delegate 是为 nil 的,所以我们才利用 delegate 修复这 个 bug,如果哪一天 delegate 不为 nil,就先不处理了。 + if (scrollView.delegate) { + return; + } + + scrollView.delegate = self.delegator; } - (void)setText:(NSString *)text { @@ -135,6 +143,7 @@ - (NSUInteger)lengthWithString:(NSString *)string { #pragma mark - Positioning Overrides +// 这样写已经可以让 sizeThatFits 时高度加上 textInsets 的值了 - (CGRect)textRectForBounds:(CGRect)bounds { bounds = CGRectInsetEdges(bounds, self.textInsets); CGRect resultRect = [super textRectForBounds:bounds]; @@ -152,6 +161,27 @@ - (CGRect)clearButtonRectForBounds:(CGRect)bounds { return result; } +#pragma mark - + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { + BOOL superReturnValue = [super canPerformAction:action withSender:sender]; + if (action == @selector(paste:) && self.canPerformPasteActionBlock) { + return self.canPerformPasteActionBlock(sender, superReturnValue); + } + return superReturnValue; +} + +- (void)paste:(id)sender { + BOOL shouldCallSuper = YES; + if (self.pasteBlock) { + shouldCallSuper = self.pasteBlock(sender); + } + if (shouldCallSuper) { + [super paste:sender]; + } +} + + @end @implementation _QMUITextFieldDelegator @@ -166,14 +196,22 @@ - (BOOL)textField:(QMUITextField *)textField shouldChangeCharactersInRange:(NSRa return YES; } - BOOL isDeleting = range.length > 0 && string.length <= 0; - if (isDeleting) { - if (NSMaxRange(range) > textField.text.length) { - // https://github.com/Tencent/QMUI_iOS/issues/377 - return NO; - } else { - return YES; + if (NSMaxRange(range) > textField.text.length) { + // 如果 range 越界了,继续返回 YES 会造成 crash + // https://github.com/Tencent/QMUI_iOS/issues/377 + // https://github.com/Tencent/QMUI_iOS/issues/1170 + // 这里的做法是本次返回 NO,并将越界的 range 缩减到没有越界的范围,再手动做该范围的替换。 + range = NSMakeRange(range.location, range.length - (NSMaxRange(range) - textField.text.length)); + if (range.length > 0) { + UITextRange *textRange = [self.textField qmui_convertUITextRangeFromNSRange:range]; + [self.textField replaceRange:textRange withText:string]; } + return NO; + } + + if (!string.length && range.length > 0) { + // 允许删除,这段必须放在上面 #377、#1170 的逻辑后面 + return YES; } NSUInteger rangeLength = textField.shouldCountingNonASCIICharacterAsTwo ? [textField.text substringWithRange:range].qmui_lengthWhenCountingNonASCIICharacterAsTwo : range.length; @@ -183,7 +221,20 @@ - (BOOL)textField:(QMUITextField *)textField shouldChangeCharactersInRange:(NSRa if (substringLength > 0 && [textField lengthWithString:string] > substringLength) { NSString *allowedText = [string qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, substringLength) lessValue:YES countingNonASCIICharacterAsTwo:textField.shouldCountingNonASCIICharacterAsTwo]; if ([textField lengthWithString:allowedText] <= substringLength) { + BOOL shouldChange = YES; + if ([textField.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:originalValue:)]) { + shouldChange = [textField.delegate textField:textField shouldChangeCharactersInRange:range replacementString:allowedText originalValue:YES]; + } + if (!shouldChange) { + return NO; + } textField.text = [textField.text stringByReplacingCharactersInRange:range withString:allowedText]; + // 通过代码 setText: 修改的文字,默认光标位置会在插入的文字开头,通常这不符合预期,因此这里将光标定位到插入的那段字符串的末尾 + // 注意由于粘贴后系统也会在下一个 runloop 去修改光标位置,所以我们这里也要 dispatch 到下一个 runloop 才能生效,否则会被系统的覆盖 + // https://github.com/Tencent/QMUI_iOS/issues/1282 + dispatch_async(dispatch_get_main_queue(), ^{ + textField.qmui_selectedRange = NSMakeRange(range.location + allowedText.length, 0); + }); if (!textField.shouldResponseToProgrammaticallyTextChanges) { [textField fireTextDidChangeEventForTextField:textField]; @@ -198,6 +249,10 @@ - (BOOL)textField:(QMUITextField *)textField shouldChangeCharactersInRange:(NSRa } } + if ([textField.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:originalValue:)]) { + return [textField.delegate textField:textField shouldChangeCharactersInRange:range replacementString:string originalValue:YES]; + } + return YES; } @@ -205,10 +260,28 @@ - (void)handleTextChangeEvent:(QMUITextField *)textField { // 1、iOS 10 以下的版本,从中文输入法的候选词里选词输入,是不会走到 textField:shouldChangeCharactersInRange:replacementString: 的,所以要在这里截断文字 // 2、如果是中文输入法正在输入拼音的过程中(markedTextRange 不为 nil),是不应该限制字数的(例如输入“huang”这5个字符,其实只是为了输入“黄”这一个字符),所以在 shouldChange 那边不会限制,而是放在 didChange 这里限制。 + // 系统的三指撤销在文本框达到最大字符长度限制时可能引发 crash + // https://github.com/Tencent/QMUI_iOS/issues/1168 + if (textField.maximumTextLength < NSUIntegerMax && (textField.undoManager.undoing || textField.undoManager.redoing)) { + return; + } + if (!textField.markedTextRange) { if ([textField lengthWithString:textField.text] > textField.maximumTextLength) { - textField.text = [textField.text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, textField.maximumTextLength) lessValue:YES countingNonASCIICharacterAsTwo:textField.shouldCountingNonASCIICharacterAsTwo]; - + NSString *text = nil; + NSInteger lastLength = textField.text.length - NSMaxRange(textField.qmui_selectedRange);// selectedRange 是系统的,所以这里按 shouldCountingNonASCIICharacterAsTwo = NO 来计算 + if (lastLength > 0) { + // 光标在中间就触发了最长文本限制,要从前面截断,不要影响光标后面的原始文本 + NSString *lastText = [textField.text substringFromIndex:NSMaxRange(textField.qmui_selectedRange)]; + NSInteger lastLengthInQMUI = [textField lengthWithString:lastText]; + NSInteger preLengthInQMUI = textField.maximumTextLength - lastLengthInQMUI; + NSString *preText = [textField.text qmui_substringAvoidBreakingUpCharacterSequencesToIndex:preLengthInQMUI lessValue:YES countingNonASCIICharacterAsTwo:textField.shouldCountingNonASCIICharacterAsTwo]; + text = [preText stringByAppendingString:lastText]; + } else { + text = [textField.text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, textField.maximumTextLength) lessValue:YES countingNonASCIICharacterAsTwo:textField.shouldCountingNonASCIICharacterAsTwo]; + } + textField.text = text; + textField.qmui_selectedRange = NSMakeRange(textField.text.length - lastLength, 0); if ([textField.delegate respondsToSelector:@selector(textField:didPreventTextChangeInRange:replacementString:)]) { [textField.delegate textField:textField didPreventTextChangeInRange:textField.qmui_selectedRange replacementString:nil]; } diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITextView.h b/QMUI/QMUIKit/QMUIComponents/QMUITextView.h index a3ac201f..f6c59915 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITextView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUITextView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITextView.h @@ -34,12 +34,17 @@ - (BOOL)textViewShouldReturn:(QMUITextView *)textView; /** - * 配合 `maximumTextLength` 属性使用,在输入文字超过限制时被调用。例如如果你的输入框在按下键盘“Done”按键时做一些发送操作,就可以在这个方法里判断 [replacementText isEqualToString:@"\n"]。 - * @warning 在 textViewDidChange: 里也会触发文字长度拦截,由于此时 textView 的文字已经改变完,所以无法得知发生改变的文本位置及改变的文本内容,所以此时 range 和 replacementText 这两个参数的值也会比较特殊,具体请看参数讲解。 + 由于 maximumTextLength 的实现方式导致业务无法再重写自己的 shouldChangeCharacters,否则会丢失 maximumTextLength 的功能。所以这里提供一个额外的 delegate,在 QMUI 内部逻辑返回 YES 的时候会再询问一次这个 delegate,从而给业务提供一个机会去限制自己的输入内容。如果 QMUI 内部逻辑本身就返回 NO(例如超过了 maximumTextLength 的长度),则不会触发这个方法。 + 当输入被这个方法拦截时,由于拦截逻辑是业务自己写的,业务能轻松获取到这个拦截的时机,所以此时不会调用 textView:didPreventTextChangeInRange:replacementText:。如果有类似 tips 之类的操作,可以直接在 return NO 之前处理。 + */ +- (BOOL)textView:(QMUITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text originalValue:(BOOL)originalValue; + +/** + * 配合 `maximumTextLength` 属性使用,在输入文字超过限制时被调用(此时文字已被自动裁剪到符合最大长度要求)。 * * @param textView 触发的 textView - * @param range 要变化的文字的位置,如果在 textViewDidChange: 里,这里的 range 也即文字变化后的 range,所以可能比最大长度要大。 - * @param replacementText 要变化的文字,如果在 textViewDidChange: 里,这里永远传入 nil。 + * @param range 要变化的文字的位置,length > 0 表示文字被自动裁剪前,输入框已有一段文字被选中。 + * @param replacementText 要变化的文字 */ - (void)textView:(QMUITextView *)textView didPreventTextChangeInRange:(NSRange)range replacementText:(NSString *)replacementText; @@ -96,6 +101,11 @@ */ @property(nonatomic, assign) CGFloat maximumHeight; +/** + 在 textView:shouldChangeTextInRange:replacementText: 里可用这个属性判断当前是否点击了删除。特别注意,当输入框为空时继续点删除也会触发,且这种情况只能通过这个属性区分,无法用别的判断方式。 + */ +@property(nonatomic, assign) BOOL isDeletingDuringTextChange; + /** * 控制输入框是否要出现“粘贴”menu * @param sender 触发这次询问事件的来源 diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITextView.m b/QMUI/QMUIKit/QMUIComponents/QMUITextView.m index 642763a2..0f319661 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITextView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUITextView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITextView.m @@ -18,6 +18,7 @@ #import "NSObject+QMUI.h" #import "NSString+QMUI.h" #import "UITextView+QMUI.h" +#import "UIScrollView+QMUI.h" #import "QMUILog.h" #import "QMUIMultipleDelegates.h" @@ -38,6 +39,7 @@ @interface QMUITextView () @property(nonatomic, assign) BOOL debug; @property(nonatomic, assign) BOOL postInitializationMethodCalled; @property(nonatomic, strong) _QMUITextViewDelegator *delegator; +@property(nonatomic, assign) BOOL isSettingTextByShouldChange; @property(nonatomic, assign) BOOL shouldRejectSystemScroll;// 如果在 handleTextChanged: 里主动调整 contentOffset,则为了避免被系统的自动调整覆盖,会利用这个标记去屏蔽系统对 setContentOffset: 的调用 @property(nonatomic, strong) UILabel *placeholderLabel; @@ -48,11 +50,16 @@ @implementation QMUITextView @dynamic delegate; -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { +- (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer { + if (self = [super initWithFrame:frame textContainer:textContainer]) { [self didInitialize]; - self.tintColor = TextFieldTintColor; + if (QMUICMIActivated) { + UIColor *textColor = TextFieldTextColor; + if (textColor) { + self.textColor = textColor; + } + self.tintColor = TextFieldTintColor; + } } return self; } @@ -73,14 +80,12 @@ - (void)didInitialize { self.delegate = self.delegator; self.scrollsToTop = NO; - self.placeholderColor = UIColorPlaceholder; + if (QMUICMIActivated) self.placeholderColor = UIColorPlaceholder; self.placeholderMargins = UIEdgeInsetsZero; self.maximumHeight = CGFLOAT_MAX; self.maximumTextLength = NSUIntegerMax; self.shouldResponseToProgrammaticallyTextChanges = YES; - if (@available(iOS 11, *)) { - self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } + self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; self.placeholderLabel = [[UILabel alloc] init]; self.placeholderLabel.font = UIFontMake(kSystemTextViewDefaultFontPointSize); @@ -89,9 +94,12 @@ - (void)didInitialize { self.placeholderLabel.alpha = 0; [self addSubview:self.placeholderLabel]; + // 监听用户手工输入引发的文字变化(代码里通过 setText: 修改的不在这个监听范围内) [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTextChanged:) name:UITextViewTextDidChangeNotification object:nil]; self.postInitializationMethodCalled = YES; + + [self hookKeyboardDeleteEventIfNeeded]; } - (void)dealloc { @@ -111,80 +119,45 @@ - (BOOL)isCurrentTextDifferentOfText:(NSString *)text { return YES; } -- (void)setText:(NSString *)text { - NSString *textBeforeChange = self.text; - BOOL textDifferent = [self isCurrentTextDifferentOfText:text]; - - // 如果前后文字没变化,则什么都不做 - if (!textDifferent) { - [super setText:text]; - return; - } - - // 前后文字发生变化,则要根据是否主动接管 delegate 来决定是否要询问 delegate - if (self.shouldResponseToProgrammaticallyTextChanges) { - BOOL shouldChangeText = YES; - if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) { - shouldChangeText = [self.delegate textView:self shouldChangeTextInRange:NSMakeRange(0, textBeforeChange.length) replacementText:text]; - } - - if (!shouldChangeText) { - // 不应该改变文字,所以连 super 都不调用,直接结束方法 - return; - } - - // 应该改变文字,则调用 super 来改变文字,然后主动调用 textViewDidChange: - [super setText:text]; - if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) { - [self.delegate textViewDidChange:self]; - } - - [[NSNotificationCenter defaultCenter] postNotificationName:UITextViewTextDidChangeNotification object:self]; - - } else { - [super setText:text]; - - // 如果不需要主动接管事件,则只要触发内部的监听即可,不用调用 delegate 系列方法 - [self handleTextChanged:self]; +- (void)_qmui_setTextForShouldChange:(NSString *)text { + self.isSettingTextByShouldChange = YES; + [self setText:text]; + // 对于 shouldResponseToProgrammaticallyTextChanges = YES 的,调用 textViewDidChange: 的工作已经在 self setText: 里做完了,所以这里对 shouldResponseToProgrammaticallyTextChanges = NO 的专门做一次 + if (!self.shouldResponseToProgrammaticallyTextChanges && [self.delegate respondsToSelector:@selector(textViewDidChange:)]) { + [self.delegate textViewDidChange:self]; } + self.isSettingTextByShouldChange = NO; } - (void)setAttributedText:(NSAttributedString *)attributedText { - NSString *textBeforeChange = self.attributedText.string; BOOL textDifferent = [self isCurrentTextDifferentOfText:attributedText.string]; - // 如果前后文字没变化,则什么都不做 if (!textDifferent) { [super setAttributedText:attributedText]; return; } - // 前后文字发生变化,则要根据是否主动接管 delegate 来决定是否要询问 delegate if (self.shouldResponseToProgrammaticallyTextChanges) { - BOOL shouldChangeText = YES; - if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) { - shouldChangeText = [self.delegate textView:self shouldChangeTextInRange:NSMakeRange(0, textBeforeChange.length) replacementText:attributedText.string]; - } - - if (!shouldChangeText) { - // 不应该改变文字,所以连 super 都不调用,直接结束方法 - return; + if (!self.isSettingTextByShouldChange) { + BOOL shouldChangeText = YES; + if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) { + NSString *textBeforeChange = self.attributedText.string; + shouldChangeText = [self.delegate textView:self shouldChangeTextInRange:NSMakeRange(0, textBeforeChange.length) replacementText:attributedText.string]; + } + + if (!shouldChangeText) { + return; + } } - - // 应该改变文字,则调用 super 来改变文字,然后主动调用 textViewDidChange: [super setAttributedText:attributedText]; if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) { [self.delegate textViewDidChange:self]; } - - [[NSNotificationCenter defaultCenter] postNotificationName:UITextViewTextDidChangeNotification object:self]; - } else { [super setAttributedText:attributedText]; - - // 如果不需要主动接管事件,则只要触发内部的监听即可,不用调用 delegate 系列方法 - [self handleTextChanged:self]; } + + [self handleTextChanged:self]; } - (void)setTypingAttributes:(NSDictionary *)typingAttributes { @@ -207,24 +180,15 @@ - (void)setTextAlignment:(NSTextAlignment)textAlignment { [self updatePlaceholderStyle]; } -- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset { - [super setTextContainerInset:textContainerInset]; - if (@available(iOS 11, *)) { - } else { - // iOS 11 以下修改 textContainerInset 的时候无法自动触发 layoutSubview,导致 placeholderLabel 无法更新布局 - [self setNeedsLayout]; - } -} - - - (void)setPlaceholder:(NSString *)placeholder { _placeholder = placeholder; - self.placeholderLabel.attributedText = [[NSAttributedString alloc] initWithString:_placeholder attributes:self.typingAttributes]; + self.placeholderLabel.attributedText = placeholder ? [[NSAttributedString alloc] initWithString:_placeholder attributes:self.typingAttributes] : nil; if (self.placeholderColor) { self.placeholderLabel.textColor = self.placeholderColor; } [self sendSubviewToBack:self.placeholderLabel]; [self setNeedsLayout]; + [self updatePlaceholderLabelHidden]; } - (void)setPlaceholderColor:(UIColor *)placeholderColor { @@ -236,14 +200,29 @@ - (void)updatePlaceholderStyle { self.placeholder = self.placeholder;// 触发文字样式的更新 } -- (void)handleTextChanged:(id)sender { - // 输入字符的时候,placeholder隐藏 - if(self.placeholder.length > 0) { - [self updatePlaceholderLabelHidden]; +- (void)updatePlaceholderLabelHidden { + if (self.text.length == 0 && self.placeholder.length > 0) { + self.placeholderLabel.alpha = 1; + } else { + self.placeholderLabel.alpha = 0;// 用alpha来让placeholder隐藏,从而尽量避免因为显隐 placeholder 导致 layout } +} + +- (CGRect)preferredPlaceholderFrameWithSize:(CGSize)size { + if (self.placeholder.length <= 0) return CGRectZero; + UIEdgeInsets allInsets = self.allInsets; + UIEdgeInsets labelMargins = UIEdgeInsetsMake(allInsets.top - self.adjustedContentInset.top, allInsets.left - self.adjustedContentInset.left, allInsets.bottom - self.adjustedContentInset.bottom, allInsets.right - self.adjustedContentInset.right); + CGFloat limitWidth = size.width - UIEdgeInsetsGetHorizontalValue(allInsets); + CGFloat limitHeight = size.height - UIEdgeInsetsGetVerticalValue(allInsets); + CGSize labelSize = [self.placeholderLabel sizeThatFits:CGSizeMake(limitWidth, limitHeight)]; + labelSize.width = limitWidth == CGFLOAT_MAX ? MIN(limitWidth, labelSize.width) : limitWidth;// 当 limitWidth 为 CGFLOAT_MAX 时,意味着此时是 sizeToFit 触发的 sizeThatFits:,从而调用到这里,此时语义上希望得到 placeholder 的实际内容宽高,于是拿 labelSize.width 作为返回值。如果不是那边过来的,则让 placeholderLabel 宽度撑满,从而适配 NSTextAlignmentRight。 + labelSize.height = MIN(limitHeight, labelSize.height); + return CGRectFlatMake(labelMargins.left, labelMargins.top, labelSize.width, labelSize.height); +} + +- (void)handleTextChanged:(id)sender { QMUITextView *textView = nil; - if ([sender isKindOfClass:[NSNotification class]]) { id object = ((NSNotification *)sender).object; if (object == self) { @@ -253,46 +232,81 @@ - (void)handleTextChanged:(id)sender { textView = (QMUITextView *)sender; } - if (textView) { - - if (!textView.editable) { - return;// 不可编辑的 textView 不会显示光标 + if (!textView) return; + + // 输入字符的时候,placeholder隐藏 + if (self.placeholder.length > 0) { + [self updatePlaceholderLabelHidden]; + } + + // 系统的三指撤销在文本框达到最大字符长度限制时可能引发 crash + // https://github.com/Tencent/QMUI_iOS/issues/1168 + if (textView.maximumTextLength < NSUIntegerMax && (textView.undoManager.undoing || textView.undoManager.redoing)) { + return; + } + + // 如果输入一长串中文拼音后,选择一个长度超过限制的候选词,在 textView:shouldChangeTextInRange:replacementText: 那边无法拦截,所以交给 handleTextChanged: 这边截断。这种情况会触发多次 handleTextChanged:,其中有一次是超出长度的,没办法,业务注意做好保护。 + if (!textView.markedTextRange && [textView lengthWithString:textView.text] > textView.maximumTextLength) { + NSString *finalText = [textView.text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, textView.maximumTextLength) lessValue:YES countingNonASCIICharacterAsTwo:textView.shouldCountingNonASCIICharacterAsTwo]; + NSString *replacementText = [textView.text substringFromIndex:finalText.length]; + textView.text = finalText; + if ([textView.delegate respondsToSelector:@selector(textView:didPreventTextChangeInRange:replacementText:)]) { + // 如果是在这里被截断,是无法得知截断前光标所处的位置及要输入的文本的,所以只能将当前的 selectedRange 传过去,而 replacementText 为 nil + [textView.delegate textView:textView didPreventTextChangeInRange:textView.selectedRange replacementText:replacementText]; } + } + + if (!textView.editable) { + return;// 不可编辑的 textView 不会显示光标 + } + + // 计算高度 + if ([textView.delegate respondsToSelector:@selector(textView:newHeightAfterTextChanged:)]) { - // 计算高度 - if ([textView.delegate respondsToSelector:@selector(textView:newHeightAfterTextChanged:)]) { - - CGFloat resultHeight = flat([textView sizeThatFits:CGSizeMake(CGRectGetWidth(textView.bounds), CGFLOAT_MAX)].height); - - if (textView.debug) QMUILog(NSStringFromClass(textView.class), @"handleTextDidChange, text = %@, resultHeight = %f", textView.text, resultHeight); - - - // 通知delegate去更新textView的高度 - if (resultHeight != flat(CGRectGetHeight(textView.bounds))) { - [textView.delegate textView:textView newHeightAfterTextChanged:resultHeight]; - } - } + CGFloat resultHeight = flat([textView sizeThatFits:CGSizeMake(CGRectGetWidth(textView.bounds), CGFLOAT_MAX)].height); - // textView 尚未被展示到界面上时,此时过早进行光标调整会计算错误 - if (!textView.window) { - return; - } + if (textView.debug) QMUILog(NSStringFromClass(textView.class), @"handleTextDidChange, text = %@, resultHeight = %f", textView.text, resultHeight); - textView.shouldRejectSystemScroll = YES; - // 用 dispatch 延迟一下,因为在文字发生换行时,系统自己会做一些滚动,我们要延迟一点才能避免被系统的滚动覆盖 - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - textView.shouldRejectSystemScroll = NO; - [textView qmui_scrollCaretVisibleAnimated:NO]; - }); + + // 通知delegate去更新textView的高度 + if (resultHeight != flat(CGRectGetHeight(textView.bounds))) { + [textView.delegate textView:textView newHeightAfterTextChanged:resultHeight]; + } + } + + // textView 尚未被展示到界面上时,此时过早进行光标调整会计算错误 + if (!textView.window) { + return; } + + textView.shouldRejectSystemScroll = YES; + // 用 dispatch 延迟一下,因为在文字发生换行时,系统自己会做一些滚动,我们要延迟一点才能避免被系统的滚动覆盖 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + textView.shouldRejectSystemScroll = NO; + [textView qmui_scrollCaretVisibleAnimated:YES]; + }); } - (CGSize)sizeThatFits:(CGSize)size { - CGSize result = [super sizeThatFits:size]; + if (size.width <= 0) size.width = CGFLOAT_MAX; + if (size.height <= 0) size.height = CGFLOAT_MAX; + CGSize result = CGSizeZero; + if (self.placeholder.length > 0 && self.text.length <= 0) { + UIEdgeInsets allInsets = self.allInsets; + CGRect frame = [self preferredPlaceholderFrameWithSize:size]; + result.width = CGRectGetWidth(frame) + UIEdgeInsetsGetHorizontalValue(allInsets); + result.height = CGRectGetHeight(frame) + UIEdgeInsetsGetVerticalValue(allInsets); + } else { + result = [super sizeThatFits:size]; + } result.height = MIN(result.height, self.maximumHeight); return result; } +- (UIEdgeInsets)allInsets { + return UIEdgeInsetsConcat(UIEdgeInsetsConcat(UIEdgeInsetsConcat(self.textContainerInset, self.placeholderMargins), kSystemTextViewFixTextInsets), self.adjustedContentInset); +} + - (void)setFrame:(CGRect)frame { if (self.postInitializationMethodCalled) { // 如果没走完 didInitialize,说明 self.maximumHeight 尚未被赋初始值 CGFLOAT_MAX,此时的值为 0,就会导致调用 initWithFrame: 时高度无效,必定被指定为 0 @@ -303,7 +317,15 @@ - (void)setFrame:(CGRect)frame { // https://github.com/Tencent/QMUI_iOS/issues/557 frame = CGRectFlatted(frame); + // 系统的 UITextView 只要调用 setFrame: 不管 rect 有没有变化都会触发 setContentOffset,引起最后一行输入过程中文字抖动的问题,所以这里屏蔽掉 + BOOL sizeChanged = !CGSizeEqualToSize(frame.size, self.frame.size); + if (!sizeChanged) { + self.shouldRejectSystemScroll = YES; + } [super setFrame:frame]; + if (!sizeChanged) { + self.shouldRejectSystemScroll = NO; + } } - (void)setBounds:(CGRect)bounds { @@ -316,12 +338,8 @@ - (void)setBounds:(CGRect)bounds { - (void)layoutSubviews { [super layoutSubviews]; if (self.placeholder.length > 0) { - UIEdgeInsets labelMargins = UIEdgeInsetsConcat(UIEdgeInsetsConcat(self.textContainerInset, self.placeholderMargins), kSystemTextViewFixTextInsets); - CGFloat limitWidth = CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.contentInset) - UIEdgeInsetsGetHorizontalValue(labelMargins); - CGFloat limitHeight = CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.contentInset) - UIEdgeInsetsGetVerticalValue(labelMargins); - CGSize labelSize = [self.placeholderLabel sizeThatFits:CGSizeMake(limitWidth, limitHeight)]; - labelSize.height = fmin(limitHeight, labelSize.height); - self.placeholderLabel.frame = CGRectFlatMake(labelMargins.left, labelMargins.top, limitWidth, labelSize.height); + CGRect frame = [self preferredPlaceholderFrameWithSize:self.bounds.size]; + self.placeholderLabel.frame = frame; } } @@ -330,14 +348,6 @@ - (void)drawRect:(CGRect)rect { [self updatePlaceholderLabelHidden]; } -- (void)updatePlaceholderLabelHidden { - if (self.text.length == 0 && self.placeholder.length > 0) { - self.placeholderLabel.alpha = 1; - } else { - self.placeholderLabel.alpha = 0;// 用alpha来让placeholder隐藏,从而尽量避免因为显隐 placeholder 导致 layout - } -} - - (NSUInteger)lengthWithString:(NSString *)string { return self.shouldCountingNonASCIICharacterAsTwo ? string.qmui_lengthWhenCountingNonASCIICharacterAsTwo : string.length; } @@ -360,6 +370,33 @@ - (void)setContentOffset:(CGPoint)contentOffset { } } +- (void)hookKeyboardDeleteEventIfNeeded { + // - [UITextView keyboardInputShouldDelete:] + // - (BOOL) keyboardInputShouldDelete:(id)arg1; + SEL selector = NSSelectorFromString([NSString qmui_stringByConcat:@"keyboard", @"Input", @"ShouldDelete", @":", nil]); + if (![self respondsToSelector:selector]) { + QMUIAssert(NO, @"QMUITextView", @"-[UITextView %@] not found.", NSStringFromSelector(selector)); + return; + } + [QMUIHelper executeBlock:^{ + OverrideImplementation([QMUITextView class], selector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^BOOL(QMUITextView *selfObject, id firstArgv) { + + selfObject.isDeletingDuringTextChange = YES; + + // call super + BOOL (*originSelectorIMP)(id, SEL, id); + originSelectorIMP = (BOOL (*)(id, SEL, id))originalIMPProvider(); + BOOL result = originSelectorIMP(selfObject, originCMD, firstArgv);// 这里会触发 shouldChangeText + + selfObject.isDeletingDuringTextChange = NO; + + return result; + }; + }); + } oncePerIdentifier:@"QMUITextView delete"]; +} + #pragma mark - - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { @@ -400,19 +437,28 @@ - (BOOL)textView:(QMUITextView *)textView shouldChangeTextInRange:(NSRange)range if (textView.maximumTextLength < NSUIntegerMax) { - // 如果是中文输入法正在输入拼音的过程中(markedTextRange 不为 nil),是不应该限制字数的(例如输入“huang”这5个字符,其实只是为了输入“黄”这一个字符),所以在 shouldChange 这里不会限制,而是放在 didChange 那里限制。 + // 如果是中文输入法正在输入拼音的过程中(markedTextRange 不为 nil),是不应该限制字数的(例如输入“huang”这5个字符,其实只是为了输入“黄”这一个字符) + // 注意当点击了候选词后触发的那一次 textView:shouldChangeTextInRange:replacementText:,此时的 marktedTextRange 依然存在,尚未被清除,所以这种情况下的字符长度限制逻辑会交给 handleTextChanged: 那边处理。 if (textView.markedTextRange) { return YES; } - BOOL isDeleting = range.length > 0 && text.length <= 0; - if (isDeleting) { - if (NSMaxRange(range) > textView.text.length) { - // https://github.com/Tencent/QMUI_iOS/issues/377 - return NO; - } else { - return YES; + if (NSMaxRange(range) > textView.text.length) { + // 如果 range 越界了,继续返回 YES 会造成 rash + // https://github.com/Tencent/QMUI_iOS/issues/377 + // https://github.com/Tencent/QMUI_iOS/issues/1170 + // 这里的做法是本次返回 NO,并将越界的 range 缩减到没有越界的范围,再手动做该范围的替换。 + range = NSMakeRange(range.location, range.length - (NSMaxRange(range) - textView.text.length)); + if (range.length > 0) { + UITextRange *textRange = [self.textView qmui_convertUITextRangeFromNSRange:range]; + [self.textView replaceRange:textRange withText:text]; } + return NO; + } + + if (!text.length && range.length > 0) { + // 允许删除,这段必须放在上面 #377、#1170 的逻辑后面 + return YES; } NSUInteger rangeLength = textView.shouldCountingNonASCIICharacterAsTwo ? [textView.text substringWithRange:range].qmui_lengthWhenCountingNonASCIICharacterAsTwo : range.length; @@ -428,12 +474,21 @@ - (BOOL)textView:(QMUITextView *)textView shouldChangeTextInRange:(NSRange)range if (substringLength > 0 && [textView lengthWithString:text] > substringLength) { NSString *allowedText = [text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, substringLength) lessValue:YES countingNonASCIICharacterAsTwo:textView.shouldCountingNonASCIICharacterAsTwo]; if ([textView lengthWithString:allowedText] <= substringLength) { - textView.text = [textView.text stringByReplacingCharactersInRange:range withString:allowedText]; - textView.selectedRange = NSMakeRange(range.location + substringLength, 0); - - if (!textView.shouldResponseToProgrammaticallyTextChanges && [textView.delegate respondsToSelector:@selector(textViewDidChange:)]) { - [textView.delegate textViewDidChange:textView]; + BOOL shouldChange = YES; + if ([textView.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:originalValue:)]) { + shouldChange = [textView.delegate textView:textView shouldChangeTextInRange:range replacementText:text originalValue:shouldChange]; } + if (!shouldChange) { + return NO; + } + [textView _qmui_setTextForShouldChange:[textView.text stringByReplacingCharactersInRange:range withString:allowedText]]; + + // iOS 10 修改 selectedRange 可以让光标立即移动到新位置,但 iOS 11 及以上版本需要延迟一会才可以 + NSRange finalSelectedRange = NSMakeRange(range.location + substringLength, 0); + textView.selectedRange = finalSelectedRange; + dispatch_async(dispatch_get_main_queue(), ^{ + textView.selectedRange = finalSelectedRange; + }); } } @@ -444,27 +499,12 @@ - (BOOL)textView:(QMUITextView *)textView shouldChangeTextInRange:(NSRange)range } } - return YES; -} - -- (void)textViewDidChange:(QMUITextView *)textView { - // 1、iOS 10 以下的版本,从中文输入法的候选词里选词输入,是不会走到 textView:shouldChangeTextInRange:replacementText: 的,所以要在这里截断文字 - // 2、如果是中文输入法正在输入拼音的过程中(markedTextRange 不为 nil),是不应该限制字数的(例如输入“huang”这5个字符,其实只是为了输入“黄”这一个字符),所以在 shouldChange 那边不会限制,而是放在 didChange 这里限制。 - if (!textView.markedTextRange) { - if ([textView lengthWithString:textView.text] > textView.maximumTextLength) { - - textView.text = [textView.text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, textView.maximumTextLength) lessValue:YES countingNonASCIICharacterAsTwo:textView.shouldCountingNonASCIICharacterAsTwo]; - - if ([textView.delegate respondsToSelector:@selector(textView:didPreventTextChangeInRange:replacementText:)]) { - // 如果是在这里被截断,是无法得知截断前光标所处的位置及要输入的文本的,所以只能将当前的 selectedRange 传过去,而 replacementText 为 nil - [textView.delegate textView:textView didPreventTextChangeInRange:textView.selectedRange replacementText:nil]; - } - - if (textView.shouldResponseToProgrammaticallyTextChanges) { - return; - } - } + if ([textView.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:originalValue:)]) { + BOOL delegateValue = [textView.delegate textView:textView shouldChangeTextInRange:range replacementText:text originalValue:YES]; + return delegateValue; } + + return YES; } @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUITheme.h b/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUITheme.h new file mode 100644 index 00000000..95c8e992 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUITheme.h @@ -0,0 +1,26 @@ +/** + * 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. + */ +// +// QMUITheme.h +// QMUIKit +// +// Created by MoLice on 2019/J/20. +// + +#ifndef QMUITheme_h +#define QMUITheme_h + +#import "QMUIThemeManagerCenter.h" +#import "QMUIThemeManager.h" +#import "UIColor+QMUITheme.h" +#import "UIImage+QMUITheme.h" +#import "UIVisualEffect+QMUITheme.h" +#import "UIView+QMUITheme.h" +#import "UIViewController+QMUITheme.h" + +#endif /* QMUITheme_h */ diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.h b/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.h new file mode 100644 index 00000000..e9791ef4 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.h @@ -0,0 +1,120 @@ +/** + * 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. + */ +// +// QMUIThemeManager.h +// QMUIKit +// +// Created by MoLice on 2019/J/20. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/// 当主题发生变化时发出这个通知,会先于 UIViewController/UIView 的 qmui_themeDidChangeByManager:identifier:theme: +extern NSNotificationName const QMUIThemeDidChangeNotification; + +/** + 主题管理组件,可添加自定义的主题对象,并为每个对象指定一个专门的 identifier,当主题发生变化时,会遍历 UIViewController 和 UIView,调用每个 viewController 和每个可视 view 的 qmui_themeDidChangeByManager:identifier:theme: 方法,在里面由业务去自行根据当前主题设置不同的外观(color、image 等)。借助 QMUIThemeManagerCenter,可实现一个项目里同时存在多个维度的主题(例如全局维度存在 light/dark 2套主题,局部的某个界面存在 white/yellow/green/black 4套主题),各自互不影响,如果业务项目只需要一个维度的主题,则全都使用 QMUIThemeManagerCenter.defaultThemeManager 来获取 QMUIThemeManager 即可,如果业务有多维度主题的需求,可使用 +[QMUIThemeManagerCenter themeManagerWithName:] 生成不同的 QMUIThemeManager。 + + 详细文档请查看 GitHub Wiki + @link https://github.com/Tencent/QMUI_iOS/wiki/%E4%BD%BF%E7%94%A8-QMUITheme-%E5%AE%9E%E7%8E%B0%E6%8D%A2%E8%82%A4%E5%B9%B6%E9%80%82%E9%85%8D-iOS-13-Dark-Mode + + 关于 theme 的概念: + 1. 一个主题包含两个元素:identifier 表示主题的标志/名字,不允许重复;theme 代表主题对象本身,可以是任意的 NSObject 类型,只要业务自行规定即可。对于任意主题而言,identifier 和 theme 都不能为空,也不能重复。 + 2. 主题的增删需要通过 QMUIThemeManager 的 addThemeIdentifier:theme:、removeThemeIdentifier:/removeTheme: 来实现。 + 3. 可通过 QMUIThemeManager 的 themeIdentifiers、themes 属性来获取当前已注册的所有主题。 + 4. 可通过修改 QMUIThemeManager 的 currentThemeIdentifier、currentTheme 属性来切换当前 App 的主题,修改这两个属性的其中一个属性,内部都会同时自动修改另外一个属性,以保证两者匹配。 + + 关于 iOS 13 新增的 Dark Mode: + 1. 如果 App 只需要在 iOS 13 里才切换深色的主题,直接使用系统的方式去实现即可,无需用到 QMUIThemeManager 的任何功能,QMUIThemeManager 适用于 App 需要在全 iOS 版本里都支持相同的皮肤切换(也即 iOS 13 下系统的 Dark Mode 也只是被视为你业务的某个皮肤)。在 iOS 13 下,QMUIThemeManager 的作用只是帮你监听系统 Dark Mode 的切换,并将系统的样式转换成业务对应的主题名,后续的实际工作其实跟 iOS 12 下切换主题是一样的。 + 2. 如果要令 QMUIThemeManager 自动响应 iOS 13 的 Dark Mode,请先为 identifierForTrait 赋值,在内部根据 trait.userInterfaceStyle 的值返回对应的主题 identifier,再把 respondsSystemStyleAutomatically 改为 YES 即可。 + + 关于 App 界面响应主题变化的方式: + 组件支持三种层面来响应主题变化: + 1. UIView 层面,如果是颜色(UIColor/CGColor)变化,请使用 [UIColor qmui_colorWithThemeProvider:] 方法来创建 UIColor,以及获取该 color 对应的 CGColor,建议每个颜色对应一个 @property,然后使用 [UIView qmui_registerThemeColorProperties:] 来注册这些需要在主题变化时自动刷新样式的 property,这样的好处是对设置颜色的时机没有要求,在 init 时就设置也没问题,不需要因为实现换肤而大量修改业务原有代码。如果是非 NSObject 的变化(例如 enum/struct 或者业务代码逻辑),可重写 [UIView qmui_themeDidChangeByManager:identifier:theme:],在里面根据当前主题做代码逻辑上的区分。 + 2. UIViewController 层面,仅支持重写 [UIViewController qmui_themeDidChangeByManager:identifier:theme:] 方法来实现换肤。 + 3. NSObject 层面,可通过监听 QMUIThemeDidChangeNotification 通知,在回调里处理主题切换事件(例如将当前选择的主题持久化记录下来,下次 App 启动直接应用)。 + + 标准场景下的使用流程: + 1. App 启动时,按需初始化 theme 对象并注册到 QMUIThemeManager 里。 + 2. 根据当前用户的选择记录(例如 NSUserDefaults),通过 currentThemeIdentifier/currentTheme 指定当前的主题。 + 3. 对需要响应主题变化的界面,检查其中的所有 UIColor、CGColor 的代码,将颜色换成使用 [UIColor qmui_colorWithThemeProvider:] 创建,如果该颜色对应一个 property,则使用 [UIView qmui_registerThemeColorProperties:] 注册这个 property,如果不对应 property,则请在 qmui_themeDidChangeByManager:identifier:theme: 里重新设置该颜色。 + 4. 通过 QMUIThemeDidChangeNotification 监听主题的变化,将其持久化存储以便下次启动时应用。 + 5. 若需要响应 iOS 13 的 Dark Mode,参考 respondsSystemStyleAutomatically、identifierForTrait 的注释。 + */ +@interface QMUIThemeManager : NSObject + +- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@property(nonatomic, copy, readonly) __kindof NSObject *name; + +/// 自动响应 iOS 13 里的 Dark Mode 切换,默认为 NO。当为 YES 时,能自动监听系统 Dark Mode 的切换,并通过询问 identifierForTrait 来将当前的系统界面样式转换成业务定义的主题,剩下的事情就跟 iOS 12 及以下的系统相同了。 +/// @warning 当设置这个属性为 YES 之前,请先为 identifierForTrait 赋值。 +@property(nonatomic, assign) BOOL respondsSystemStyleAutomatically API_AVAILABLE(ios(13.0)); + +/// 当 respondsSystemStyleAutomatically 为 YES 并且系统样式发生变化时,会通过这个 block 将当前的 UITraitCollection.userInterfaceStyle 转换成对应的业务主题 identifier +@property(nonatomic, copy, nullable) __kindof NSObject *(^identifierForTrait)(UITraitCollection *trait) API_AVAILABLE(ios(13.0)); + +/// 获取所有主题的 identifier +@property(nonatomic, copy, readonly, nullable) NSArray<__kindof NSObject *> *themeIdentifiers; + +/// 获取所有主题的对象 +@property(nonatomic, copy, readonly, nullable) NSArray<__kindof NSObject *> *themes; + +/// 获取当前主题的 identifier +@property(nonatomic, copy, nullable) __kindof NSObject *currentThemeIdentifier; + +/// 获取当前主题的对象 +@property(nonatomic, strong, nullable) __kindof NSObject *currentTheme; + +/// 当切换 currentThemeIdentifier 时如果遇到该 identifier 尚未被注册,则会尝试通过这个 block 来获取对应的主题对象并添加到 QMUIThemeManager 里 +@property(nonatomic, copy, nullable) __kindof NSObject * _Nullable (^themeGenerator)(__kindof NSObject *identifier); + +/// 当切换 currentTheme 时如果遇到该 theme 尚未被注册,则会尝试通过这个 block 来获取对应的 identifier 并添加到 QMUIThemeManager 里 +@property(nonatomic, copy, nullable) __kindof NSObject * _Nullable (^themeIdentifierGenerator)(__kindof NSObject *theme); + +/** + 添加主题,不允许重复添加 + @param identifier 主题的 identifier,一般用 NSString 即可,不允许重复 + @param theme 主题的对象,允许任意 class 类型 + */ +- (void)addThemeIdentifier:(__kindof NSObject *)identifier theme:(__kindof NSObject *)theme; + +/** + 移除指定 identifier 的主题 + @param identifier 要移除的 identifier + */ +- (void)removeThemeIdentifier:(__kindof NSObject *)identifier; + +/** + 移除指定的主题对象 + @param theme 要移除的主题对象 + */ +- (void)removeTheme:(__kindof NSObject *)theme; + +/** + 根据指定的 identifier 获取对应的主题对象 + @param identifier 主题的 identifier + @return identifier 对应的主题对象 + */ +- (nullable __kindof NSObject *)themeForIdentifier:(__kindof NSObject *)identifier; + +/** + 获取主题对应的 identifier + @param theme 主题对象 + @return 主题的 identifier + */ +- (nullable __kindof NSObject *)identifierForTheme:(__kindof NSObject *)theme; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.m b/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.m new file mode 100644 index 00000000..d73a5326 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.m @@ -0,0 +1,150 @@ +/** + * 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. + */ +// +// QMUIThemeManager.m +// QMUIKit +// +// Created by MoLice on 2019/J/20. +// + +#import "QMUIThemeManager.h" +#import "QMUICore.h" +#import "UIView+QMUITheme.h" +#import "UIViewController+QMUITheme.h" +#import "QMUIThemePrivate.h" +#import "UITraitCollection+QMUI.h" + +NSString *const QMUIThemeDidChangeNotification = @"QMUIThemeDidChangeNotification"; + +@interface QMUIThemeManager () + +@property(nonatomic, strong) NSMutableArray *> *_themeIdentifiers; +@property(nonatomic, strong) NSMutableArray *_themes; +@end + +@implementation QMUIThemeManager + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@, name = %@, themes = %@", [super description], self.name, self.themes]; +} + +// 这个方法的声明放在 QMUIThemeManagerCenter.m 里,简单实现 private 的效果 +- (instancetype)initWithName:(__kindof NSObject *)name { + if (self = [super init]) { + _name = name; + self._themeIdentifiers = NSMutableArray.new; + self._themes = NSMutableArray.new; + [UITraitCollection qmui_addUserInterfaceStyleWillChangeObserver:self selector:@selector(handleUserInterfaceStyleWillChangeEvent:)]; + } + return self; +} + +- (void)handleUserInterfaceStyleWillChangeEvent:(UITraitCollection *)traitCollection { + if (!_respondsSystemStyleAutomatically) return; + if (traitCollection && self.identifierForTrait) { + self.currentThemeIdentifier = self.identifierForTrait(traitCollection); + } +} + +- (void)setRespondsSystemStyleAutomatically:(BOOL)respondsSystemStyleAutomatically { + _respondsSystemStyleAutomatically = respondsSystemStyleAutomatically; + if (_respondsSystemStyleAutomatically && self.identifierForTrait) { + self.currentThemeIdentifier = self.identifierForTrait([UITraitCollection currentTraitCollection]); + } +} + +- (void)setCurrentThemeIdentifier:(NSObject *)currentThemeIdentifier { + if (![self._themeIdentifiers containsObject:currentThemeIdentifier] && self.themeGenerator) { + NSObject *theme = self.themeGenerator(currentThemeIdentifier); + [self addThemeIdentifier:currentThemeIdentifier theme:theme]; + } + + QMUIAssert([self._themeIdentifiers containsObject:currentThemeIdentifier], @"QMUIThemeManager", @"%@ should be added to QMUIThemeManager.themes before it becomes current theme identifier.", currentThemeIdentifier); + + BOOL themeChanged = _currentThemeIdentifier && ![_currentThemeIdentifier isEqual:currentThemeIdentifier]; + + _currentThemeIdentifier = currentThemeIdentifier; + _currentTheme = [self themeForIdentifier:currentThemeIdentifier]; + + if (themeChanged) { + [self notifyThemeChanged]; + } +} + +- (void)setCurrentTheme:(NSObject *)currentTheme { + if (![self._themes containsObject:currentTheme] && self.themeIdentifierGenerator) { + __kindof NSObject *identifier = self.themeIdentifierGenerator(currentTheme); + [self addThemeIdentifier:identifier theme:currentTheme]; + } + + QMUIAssert([self._themes containsObject:currentTheme], @"QMUIThemeManager", @"%@ should be added to QMUIThemeManager.themes before it becomes current theme.", currentTheme); + + BOOL themeChanged = _currentTheme && ![_currentTheme isEqual:currentTheme]; + + _currentTheme = currentTheme; + _currentThemeIdentifier = [self identifierForTheme:currentTheme]; + + if (themeChanged) { + [self notifyThemeChanged]; + } +} + +- (NSArray *> *)themeIdentifiers { + return self._themeIdentifiers.count ? self._themeIdentifiers.copy : nil; +} + +- (NSArray *)themes { + return self._themes.count ? self._themes.copy : nil; +} + +- (__kindof NSObject *)themeForIdentifier:(__kindof NSObject *)identifier { + NSUInteger index = [self._themeIdentifiers indexOfObject:identifier]; + if (index != NSNotFound) return self._themes[index]; + return nil; +} + +- (__kindof NSObject *)identifierForTheme:(__kindof NSObject *)theme { + NSUInteger index = [self._themes indexOfObject:theme]; + if (index != NSNotFound) return self._themeIdentifiers[index]; + return nil; +} + +- (void)addThemeIdentifier:(NSObject *)identifier theme:(NSObject *)theme { + QMUIAssert(![self._themeIdentifiers containsObject:identifier], @"QMUIThemeManager", @"unable to add duplicate theme identifier"); + QMUIAssert(![self._themes containsObject:theme], @"QMUIThemeManager", @"unable to add duplicate theme"); + + [self._themeIdentifiers addObject:identifier]; + [self._themes addObject:theme]; +} + +- (void)removeThemeIdentifier:(NSObject *)identifier { + [self._themeIdentifiers removeObject:identifier]; +} + +- (void)removeTheme:(NSObject *)theme { + [self._themes removeObject:theme]; +} + +- (void)notifyThemeChanged { + [[NSNotificationCenter defaultCenter] postNotificationName:QMUIThemeDidChangeNotification object:self]; + + [UIApplication.sharedApplication.windows enumerateObjectsUsingBlock:^(__kindof UIWindow * _Nonnull window, NSUInteger idx, BOOL * _Nonnull stop) { + if (!window.hidden && window.alpha > 0.01 && window.rootViewController) { + [window.rootViewController qmui_themeDidChangeByManager:self identifier:self.currentThemeIdentifier theme:self.currentTheme]; + + // 某些 present style 情况下,window 上可能存在多个 viewController.view,因此需要遍历所有的 subviews,而不只是 window.rootViewController.view + [window _qmui_themeDidChangeByManager:self identifier:self.currentThemeIdentifier theme:self.currentTheme shouldEnumeratorSubviews:YES]; + } + }]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManagerCenter.h b/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManagerCenter.h new file mode 100644 index 00000000..f97f772a --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManagerCenter.h @@ -0,0 +1,32 @@ +/** + * 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. + */ +// +// QMUIThemeManagerCenter.h +// QMUIKit +// +// Created by MoLice on 2019/S/4. +// + +#import +#import "QMUIThemeManager.h" + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const QMUIThemeManagerNameDefault; + +/** + 用于获取 QMUIThemeManager,具体使用请查看 QMUIThemeManager 的注释。 + */ +@interface QMUIThemeManagerCenter : NSObject + +@property(class, nonatomic, strong, readonly) QMUIThemeManager *defaultThemeManager; +@property(class, nonatomic, copy, readonly) NSArray *themeManagers; ++ (nullable QMUIThemeManager *)themeManagerWithName:(__kindof NSObject *)name; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManagerCenter.m b/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManagerCenter.m new file mode 100644 index 00000000..d1638a89 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManagerCenter.m @@ -0,0 +1,64 @@ +/** + * 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. + */ +// +// QMUIThemeManagerCenter.m +// QMUIKit +// +// Created by MoLice on 2019/S/4. +// + +#import "QMUIThemeManagerCenter.h" + +NSString *const QMUIThemeManagerNameDefault = @"Default"; + +@interface QMUIThemeManager () + +// 这个方法的实现在 QMUIThemeManager.m 里,这里只是为了内部使用而显式声明一次 +- (instancetype)initWithName:(__kindof NSObject *)name; +@end + +@interface QMUIThemeManagerCenter () + +@property(nonatomic, strong) NSMutableArray *allManagers; +@end + +@implementation QMUIThemeManagerCenter + ++ (instancetype)sharedInstance { + static dispatch_once_t onceToken; + static QMUIThemeManagerCenter *instance = nil; + dispatch_once(&onceToken,^{ + instance = [[super allocWithZone:NULL] init]; + instance.allManagers = NSMutableArray.new; + }); + return instance; +} + ++ (id)allocWithZone:(struct _NSZone *)zone{ + return [self sharedInstance]; +} + ++ (QMUIThemeManager *)themeManagerWithName:(__kindof NSObject *)name { + QMUIThemeManagerCenter *center = [QMUIThemeManagerCenter sharedInstance]; + for (QMUIThemeManager *manager in center.allManagers) { + if ([manager.name isEqual:name]) return manager; + } + QMUIThemeManager *manager = [[QMUIThemeManager alloc] initWithName:name]; + [center.allManagers addObject:manager]; + return manager; +} + ++ (QMUIThemeManager *)defaultThemeManager { + return [QMUIThemeManagerCenter themeManagerWithName:QMUIThemeManagerNameDefault]; +} + ++ (NSArray *)themeManagers { + return [QMUIThemeManagerCenter sharedInstance].allManagers.copy; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.h b/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.h new file mode 100644 index 00000000..a1945986 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.h @@ -0,0 +1,58 @@ +/** + * 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. + */ +// +// QMUIThemePrivate.h +// QMUIKit +// +// Created by MoLice on 2019/J/26. +// + +#import +#import +#import "UIColor+QMUI.h" +#import "UIImage+QMUITheme.h" +#import "UIVisualEffect+QMUITheme.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface UIView (QMUITheme_Private) + +// 某些 view class 在遇到 qmui_registerThemeColorProperties: 无法满足 theme 变化时的刷新需求时,可以重写这个方法来做自己的逻辑 +- (void)_qmui_themeDidChangeByManager:(nullable QMUIThemeManager *)manager identifier:(nullable __kindof NSObject *)identifier theme:(nullable __kindof NSObject *)theme shouldEnumeratorSubviews:(BOOL)shouldEnumeratorSubviews; + +/// 记录当前 view 总共有哪些 property 需要在 theme 变化时重新设置 +@property(nonatomic, strong) NSMutableDictionary *qmuiTheme_themeColorProperties; + +- (BOOL)_qmui_visible; + +@end + +/// @warning 由于支持 NSCopying,增加属性时必须在 copyWithZone: 里复制一次 +@interface QMUIThemeColor : UIColor + +@property(nonatomic, copy, nullable) NSString *name; +@property(nonatomic, copy) NSObject *managerName; +@property(nonatomic, copy) UIColor *(^themeProvider)(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme); +@end + +@interface QMUIThemeImage : UIImage + +@property(nonatomic, copy, nullable) NSString *name; +@property(nonatomic, copy) NSObject *managerName; +@property(nonatomic, copy) UIImage *(^themeProvider)(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme); +@end + +/// @warning 由于支持 NSCopying,增加属性时必须在 copyWithZone: 里复制一次 +@interface QMUIThemeVisualEffect : NSObject + +@property(nonatomic, copy, nullable) NSString *name; +@property(nonatomic, copy) NSObject *managerName; +@property(nonatomic, copy) __kindof UIVisualEffect *(^themeProvider)(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme); +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m b/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m new file mode 100644 index 00000000..558dcae2 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m @@ -0,0 +1,538 @@ +/** + * 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. + */ +// +// QMUIThemePrivate.m +// QMUIKit +// +// Created by MoLice on 2019/J/26. +// + +#import "QMUIThemePrivate.h" +#import "QMUICore.h" +#import "UIColor+QMUI.h" +#import "UIVisualEffect+QMUITheme.h" +#import "UIView+QMUITheme.h" +#import "UISlider+QMUI.h" +#import "UIView+QMUI.h" +#import "UISearchBar+QMUI.h" +#import "UITableViewCell+QMUI.h" +#import "CALayer+QMUI.h" +#import "UIVisualEffectView+QMUI.h" +#import "UIBarItem+QMUI.h" +#import "UITabBar+QMUI.h" +#import "UITabBarItem+QMUI.h" + +// QMUI classes +#import "QMUIImagePickerCollectionViewCell.h" +#import "QMUIAlertController.h" +#import "QMUIButton.h" +#import "QMUIConsole.h" +#import "QMUIEmotionView.h" +#import "QMUIEmptyView.h" +#import "QMUIGridView.h" +#import "QMUIImagePreviewView.h" +#import "QMUILabel.h" +#import "QMUIPopupContainerView.h" +#import "QMUIPopupMenuView.h" +#import "QMUITextField.h" +#import "QMUITextView.h" +#import "QMUIToastBackgroundView.h" +#import "QMUIBadgeProtocol.h" + +@interface QMUIThemePropertiesRegister : NSObject + +@end + +@implementation QMUIThemePropertiesRegister + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ExtendImplementationOfNonVoidMethodWithSingleArgument([UIView class], @selector(initWithFrame:), CGRect, UIView *, ^UIView *(UIView *selfObject, CGRect frame, UIView *originReturnValue) { + ({ + static NSDictionary *> *classRegisters = nil; + if (!classRegisters) { + classRegisters = @{ + NSStringFromClass(UISlider.class): @[NSStringFromSelector(@selector(minimumTrackTintColor)), + NSStringFromSelector(@selector(maximumTrackTintColor)), + NSStringFromSelector(@selector(thumbTintColor)), + NSStringFromSelector(@selector(qmui_thumbColor))], + NSStringFromClass(UISwitch.class): @[NSStringFromSelector(@selector(onTintColor)), + NSStringFromSelector(@selector(thumbTintColor)),], + NSStringFromClass(UIActivityIndicatorView.class): @[NSStringFromSelector(@selector(color)),], + NSStringFromClass(UIProgressView.class): @[NSStringFromSelector(@selector(progressTintColor)), + NSStringFromSelector(@selector(trackTintColor)),], + NSStringFromClass(UIPageControl.class): @[NSStringFromSelector(@selector(pageIndicatorTintColor)), + NSStringFromSelector(@selector(currentPageIndicatorTintColor)),], + NSStringFromClass(UITableView.class): @[NSStringFromSelector(@selector(backgroundColor)), + NSStringFromSelector(@selector(sectionIndexColor)), + NSStringFromSelector(@selector(sectionIndexBackgroundColor)), + NSStringFromSelector(@selector(sectionIndexTrackingBackgroundColor)), + NSStringFromSelector(@selector(separatorColor)),], + NSStringFromClass(UITableViewCell.class): @[NSStringFromSelector(@selector(qmui_selectedBackgroundColor)),], + NSStringFromClass(UICollectionViewCell.class): @[NSStringFromSelector(@selector(qmui_selectedBackgroundColor)),], + NSStringFromClass(UINavigationBar.class): ({ + NSMutableArray *result = @[ + NSStringFromSelector(@selector(qmui_effect)), + NSStringFromSelector(@selector(qmui_effectForegroundColor)), + ].mutableCopy; + if (@available(iOS 15.0, *)) { + // iOS 15 在 UINavigationBar (QMUI) 里对所有旧版接口都映射到 standardAppearance,所以重新设置一次 standardAppearance 就可以更新所有样式 + [result addObject:NSStringFromSelector(@selector(standardAppearance))]; + } else { + [result addObjectsFromArray:@[NSStringFromSelector(@selector(barTintColor)),]]; + } + result.copy; + }), + NSStringFromClass(UIToolbar.class): @[NSStringFromSelector(@selector(barTintColor)),], + NSStringFromClass(UITabBar.class): @[ + NSStringFromSelector(@selector(qmui_effect)), + NSStringFromSelector(@selector(qmui_effectForegroundColor)), + NSStringFromSelector(@selector(standardAppearance)), + ], + NSStringFromClass(UISearchBar.class): @[NSStringFromSelector(@selector(barTintColor)), + NSStringFromSelector(@selector(qmui_placeholderColor)), + NSStringFromSelector(@selector(qmui_textColor)),], + NSStringFromClass(UITextField.class): @[NSStringFromSelector(@selector(attributedText)),], + NSStringFromClass(UIView.class): @[NSStringFromSelector(@selector(tintColor)), + NSStringFromSelector(@selector(backgroundColor)), + NSStringFromSelector(@selector(qmui_borderColor)), + NSStringFromSelector(@selector(qmui_badgeBackgroundColor)), + NSStringFromSelector(@selector(qmui_badgeTextColor)), + NSStringFromSelector(@selector(qmui_updatesIndicatorColor)),], + NSStringFromClass(UIVisualEffectView.class): @[NSStringFromSelector(@selector(effect)), + NSStringFromSelector(@selector(qmui_foregroundColor))], + NSStringFromClass(UIImageView.class): @[NSStringFromSelector(@selector(image))], + + // QMUI classes + NSStringFromClass(QMUIImagePickerCollectionViewCell.class): @[NSStringFromSelector(@selector(videoDurationLabelTextColor)),], + NSStringFromClass(QMUIButton.class): @[ + // tintColorAdjustsTitleAndImage 内部会设置给 tintColor,tintColor 自己会刷新,所以这里不要重复刷 + // https://github.com/Tencent/QMUI_iOS/issues/1452 + // NSStringFromSelector(@selector(tintColorAdjustsTitleAndImage)), + NSStringFromSelector(@selector(highlightedBackgroundColor)), + NSStringFromSelector(@selector(highlightedBorderColor)),], + NSStringFromClass(QMUIConsole.class): @[NSStringFromSelector(@selector(searchResultHighlightedBackgroundColor)),], + NSStringFromClass(QMUIEmotionView.class): @[NSStringFromSelector(@selector(sendButtonBackgroundColor)),], + NSStringFromClass(QMUIEmptyView.class): @[NSStringFromSelector(@selector(textLabelTextColor)), + NSStringFromSelector(@selector(detailTextLabelTextColor)), + NSStringFromSelector(@selector(actionButtonTitleColor))], + NSStringFromClass(QMUIGridView.class): @[NSStringFromSelector(@selector(separatorColor)),], + NSStringFromClass(QMUIImagePreviewView.class): @[NSStringFromSelector(@selector(loadingColor)),], + NSStringFromClass(QMUILabel.class): @[NSStringFromSelector(@selector(highlightedBackgroundColor)),], + NSStringFromClass(QMUIPopupContainerView.class): @[NSStringFromSelector(@selector(highlightedBackgroundColor)), + NSStringFromSelector(@selector(maskViewBackgroundColor)), + NSStringFromSelector(@selector(borderColor)), + NSStringFromSelector(@selector(arrowImage)),], + NSStringFromClass(QMUIPopupMenuView.class): @[NSStringFromSelector(@selector(itemSeparatorColor)), + NSStringFromSelector(@selector(sectionSeparatorColor)), + NSStringFromSelector(@selector(sectionSpacingColor)),], + NSStringFromClass(QMUIPopupMenuItemView.class): @[NSStringFromSelector(@selector(highlightedBackgroundColor))], + NSStringFromClass(QMUITextField.class): @[NSStringFromSelector(@selector(placeholderColor)),], + NSStringFromClass(QMUITextView.class): @[NSStringFromSelector(@selector(placeholderColor)),], + NSStringFromClass(QMUIToastBackgroundView.class): @[NSStringFromSelector(@selector(styleColor)),], + + // 以下的 class 的更新依赖于 UIView (QMUITheme) 内的 setNeedsDisplay,这里不专门调用 setter +// NSStringFromClass(UILabel.class): @[NSStringFromSelector(@selector(textColor)), +// NSStringFromSelector(@selector(shadowColor)), +// NSStringFromSelector(@selector(highlightedTextColor)),], +// NSStringFromClass(UITextView.class): @[NSStringFromSelector(@selector(attributedText)),], + + }; + } + [classRegisters enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull classString, NSArray * _Nonnull getters, BOOL * _Nonnull stop) { + if ([selfObject isKindOfClass:NSClassFromString(classString)]) { + [selfObject qmui_registerThemeColorProperties:getters]; + } + }]; + }); + return originReturnValue; + }); + }); +} + ++ (void)registerToClass:(Class)class byBlock:(void (^)(UIView *view))block withView:(UIView *)view { + if ([view isKindOfClass:class]) { + block(view); + } +} + +@end + +@implementation UIView (QMUIThemeCompatibility) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // iOS 12 及以下,-[UIView setTintColor:] 被调用时,如果参数的 tintColor 与当前的 tintColor 指针相同,则不会触发 tintColorDidChange,但这对于 dynamic color 而言是不满足需求的(同一个 dynamic color 实例在任何时候返回的 rawColor 都有可能发生变化),所以这里主动为其做一次 copy 操作,规避指针地址判断的问题 + // 2022-7-20 后来发现 iOS 13-15,UIImageView、UIButton,手动切换 theme 时,tintColor 不 copy 就无法刷新,但如果是系统 Dark Mode 切换引发的 setTintColor:,即便不用 copy 也可以刷新,所以这里统一对所有 iOS 版本都做一次 copy + // https://github.com/Tencent/QMUI_iOS/issues/1418 + OverrideImplementation([UIView class], @selector(setTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, UIColor *tintColor) { + + if (tintColor.qmui_isQMUIDynamicColor && tintColor == selfObject.tintColor) tintColor = tintColor.copy; + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, tintColor); + }; + }); + }); +} + +@end + +@implementation UISwitch (QMUIThemeCompatibility) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // 这里反而是 iOS 13 才需要用 copy 的方式强制触发更新,否则如果某个 UISwitch 处于 off 的状态,此时去更新它的 onTintColor 不会立即生效,而是要等切换到 on 时,才会看到旧的 onTintColor 一闪而过变成新的 onTintColor,所以这里加个强制刷新 + OverrideImplementation([UISwitch class], @selector(setOnTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISwitch *selfObject, UIColor *tintColor) { + + if (tintColor.qmui_isQMUIDynamicColor && tintColor == selfObject.onTintColor) tintColor = tintColor.copy; + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, tintColor); + }; + }); + + OverrideImplementation([UISwitch class], @selector(setThumbTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISwitch *selfObject, UIColor *tintColor) { + + if (tintColor.qmui_isQMUIDynamicColor && tintColor == selfObject.thumbTintColor) tintColor = tintColor.copy; + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, tintColor); + }; + }); + }); +} + + +@end + +@implementation UISlider (QMUIThemeCompatibility) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation([UISlider class], @selector(setMinimumTrackTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISlider *selfObject, UIColor *tintColor) { + + if (tintColor.qmui_isQMUIDynamicColor && tintColor == selfObject.minimumTrackTintColor) tintColor = tintColor.copy; + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, tintColor); + }; + }); + + OverrideImplementation([UISlider class], @selector(setMaximumTrackTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISlider *selfObject, UIColor *tintColor) { + + if (tintColor.qmui_isQMUIDynamicColor && tintColor == selfObject.maximumTrackTintColor) tintColor = tintColor.copy; + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, tintColor); + }; + }); + + OverrideImplementation([UISlider class], @selector(setThumbTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISlider *selfObject, UIColor *tintColor) { + + if (tintColor.qmui_isQMUIDynamicColor && tintColor == selfObject.thumbTintColor) tintColor = tintColor.copy; + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, tintColor); + }; + }); + }); +} + +@end + +@implementation UIProgressView (QMUIThemeCompatibility) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation([UIProgressView class], @selector(setProgressTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIProgressView *selfObject, UIColor *tintColor) { + + if (tintColor.qmui_isQMUIDynamicColor && tintColor == selfObject.progressTintColor) tintColor = tintColor.copy; + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, tintColor); + }; + }); + + OverrideImplementation([UIProgressView class], @selector(setTrackTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIProgressView *selfObject, UIColor *tintColor) { + + if (tintColor.qmui_isQMUIDynamicColor && tintColor == selfObject.trackTintColor) tintColor = tintColor.copy; + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, tintColor); + }; + }); + }); +} + +@end + +@implementation UITabBarItem (QMUIThemeCompatibility) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // UITabBarItem.image 会一直保存原始的 image(例如 QMUIThemeImage),但 selectedImage 只会返回 rawImage,这导致了将一个 QMUIThemeImage 设置给 selectedImage 后,主题切换后 selectedImage 无法刷新(因为 UITabBarItem 并没有保存它,保存的是它的 rawImage),所以这里自己保存 image 的引用。 + // https://github.com/Tencent/QMUI_iOS/issues/1122 + OverrideImplementation([UITabBarItem class], @selector(setSelectedImage:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITabBarItem *selfObject, UIImage *selectedImage) { + + // 必须先保存起来再执行 super,因为 setter 的 super 里会触发 getter,如果不先保存,就会导致走到 getter 时拿到的 boundObject 还是旧值 + // https://github.com/Tencent/QMUI_iOS/issues/1218 + [selfObject qmui_bindObject:selectedImage.qmui_isDynamicImage ? selectedImage : nil forKey:@"UITabBarItem(QMUIThemeCompatibility).selectedImage"]; + + // call super + void (*originSelectorIMP)(id, SEL, UIImage *); + originSelectorIMP = (void (*)(id, SEL, UIImage *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, selectedImage); + }; + }); + + OverrideImplementation([UITabBarItem class], @selector(selectedImage), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UITabBarItem *selfObject) { + + // call super + UIImage * (*originSelectorIMP)(id, SEL); + originSelectorIMP = (UIImage * (*)(id, SEL))originalIMPProvider(); + UIImage *result = originSelectorIMP(selfObject, originCMD); + + UIImage *selectedImage = [selfObject qmui_getBoundObjectForKey:@"UITabBarItem(QMUIThemeCompatibility).selectedImage"]; + if (selectedImage) { + return selectedImage; + } + + return result; + }; + }); + }); +} + +@end + +@implementation UIVisualEffectView (QMUIThemeCompatibility) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation([UIVisualEffectView class], @selector(setEffect:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIVisualEffectView *selfObject, UIVisualEffect *effect) { + + if (effect.qmui_isDynamicEffect && effect == selfObject.effect) effect = effect.copy; + + // call super + void (*originSelectorIMP)(id, SEL, UIVisualEffect *); + originSelectorIMP = (void (*)(id, SEL, UIVisualEffect *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, effect); + }; + }); + }); +} + +@end + +@interface CALayer () + +@property(nonatomic, strong) UIColor *qcl_originalBackgroundColor; +@property(nonatomic, strong) UIColor *qcl_originalBorderColor; +@property(nonatomic, strong) UIColor *qcl_originalShadowColor; + +@end + +@implementation CALayer (QMUIThemeCompatibility) + +QMUISynthesizeIdStrongProperty(qcl_originalBackgroundColor, setQcl_originalBackgroundColor) +QMUISynthesizeIdStrongProperty(qcl_originalBorderColor, setQcl_originalBorderColor) +QMUISynthesizeIdStrongProperty(qcl_originalShadowColor, setQcl_originalShadowColor) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + OverrideImplementation([CALayer class], @selector(setBackgroundColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(CALayer *selfObject, CGColorRef color) { + + // 这里是为了让 CGColor 也支持动态颜色 + // iOS 13 的 UIDynamicProviderColor,以及 QMUIThemeColor 在获取 CGColor 时会将自身绑定到 CGColorRef 上,这里把原始的 color 重新获取出来存到 property 里,以备样式更新时调用 + UIColor *originalColor = [(__bridge id)(color) qmui_getBoundObjectForKey:QMUICGColorOriginalColorBindKey]; + selfObject.qcl_originalBackgroundColor = originalColor; + + // call super + void (*originSelectorIMP)(id, SEL, CGColorRef); + originSelectorIMP = (void (*)(id, SEL, CGColorRef))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, color); + }; + }); + + OverrideImplementation([CALayer class], @selector(setBorderColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(CALayer *selfObject, CGColorRef color) { + + UIColor *originalColor = [(__bridge id)(color) qmui_getBoundObjectForKey:QMUICGColorOriginalColorBindKey]; + selfObject.qcl_originalBorderColor = originalColor; + + // call super + void (*originSelectorIMP)(id, SEL, CGColorRef); + originSelectorIMP = (void (*)(id, SEL, CGColorRef))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, color); + }; + }); + + OverrideImplementation([CALayer class], @selector(setShadowColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(CALayer *selfObject, CGColorRef color) { + + UIColor *originalColor = [(__bridge id)(color) qmui_getBoundObjectForKey:QMUICGColorOriginalColorBindKey]; + selfObject.qcl_originalShadowColor = originalColor; + + // call super + void (*originSelectorIMP)(id, SEL, CGColorRef); + originSelectorIMP = (void (*)(id, SEL, CGColorRef))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, color); + }; + }); + + // iOS 13 下,如果系统的主题发生变化,会自动调用每个 view 的 layoutSubviews,所以我们在这里面自动更新样式 + // 如果是 QMUIThemeManager 引发的主题变化,会在 theme 那边主动调用 qmui_setNeedsUpdateDynamicStyle,就不依赖这里 + ExtendImplementationOfVoidMethodWithoutArguments([UIView class], @selector(layoutSubviews), ^(UIView *selfObject) { + [selfObject.layer qmui_setNeedsUpdateDynamicStyle]; + }); + }); +} + +/// 这里的逻辑用于让 CGColor 也支持响应 +- (void)qmui_setNeedsUpdateDynamicStyle { + if (self.qcl_originalBackgroundColor) { + UIColor *originalColor = self.qcl_originalBackgroundColor; + self.backgroundColor = originalColor.CGColor; + } + if (self.qcl_originalBorderColor) { + self.borderColor = self.qcl_originalBorderColor.CGColor; + } + if (self.qcl_originalShadowColor) { + self.shadowColor = self.qcl_originalShadowColor.CGColor; + } + + [self.sublayers enumerateObjectsUsingBlock:^(__kindof CALayer * _Nonnull sublayer, NSUInteger idx, BOOL * _Nonnull stop) { + if (!sublayer.qmui_isRootLayerOfView) {// 如果是 UIView 的 rootLayer,它会依赖 UIView 树自己的 layoutSubviews 去逐个触发,不需要手动遍历到,这里只需要遍历那些额外添加到 layer 上的 sublayer 即可 + [sublayer qmui_setNeedsUpdateDynamicStyle]; + } + }]; +} + +@end + +@interface UISearchBar () + +@property(nonatomic, readonly) NSMutableDictionary *qmuiTheme_invocations; + +@end + +@implementation UISearchBar (QMUIThemeCompatibility) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + OverrideImplementation([UISearchBar class], @selector(setSearchFieldBackgroundImage:forState:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + + NSMethodSignature *methodSignature = [originClass instanceMethodSignatureForSelector:originCMD]; + + return ^(UISearchBar *selfObject, UIImage *image, UIControlState state) { + + void (*originSelectorIMP)(id, SEL, UIImage *, UIControlState); + originSelectorIMP = (void (*)(id, SEL, UIImage *, UIControlState))originalIMPProvider(); + + UIImage *previousImage = [selfObject searchFieldBackgroundImageForState:state]; + if (previousImage.qmui_isDynamicImage || image.qmui_isDynamicImage) { + // setSearchFieldBackgroundImage:forState: 的内部实现原理: + // 执行后将 image 先存起来,在 layout 时会调用 -[UITextFieldBorderView setImage:] 该方法内部有一个判断: + // if (UITextFieldBorderView._image == image) return + // 由于 QMUIDynamicImage 随时可能发生图片的改变,这里要绕过这个判断:必须先清空一下 image,并马上调用 layoutIfNeeded 触发 -[UITextFieldBorderView setImage:] 使得 UITextFieldBorderView 内部的 image 清空,这样再设置新的才会生效。 + originSelectorIMP(selfObject, originCMD, UIImage.new, state); + [selfObject.searchTextField setNeedsLayout]; + [selfObject.searchTextField layoutIfNeeded]; + } + originSelectorIMP(selfObject, originCMD, image, state); + + NSInvocation *invocation = nil; + NSString *invocationActionKey = [NSString stringWithFormat:@"%@-%zd", NSStringFromSelector(originCMD), state]; + if (image.qmui_isDynamicImage) { + invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + [invocation setSelector:originCMD]; + [invocation setArgument:&image atIndex:2]; + [invocation setArgument:&state atIndex:3]; + [invocation retainArguments]; + } + selfObject.qmuiTheme_invocations[invocationActionKey] = invocation; + }; + }); + + OverrideImplementation([UISearchBar class], @selector(setBarTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISearchBar *selfObject, UIColor *barTintColor) { + + if (barTintColor.qmui_isQMUIDynamicColor && barTintColor == selfObject.barTintColor) barTintColor = barTintColor.copy; + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, barTintColor); + }; + }); + }); +} + +- (void)_qmui_themeDidChangeByManager:(QMUIThemeManager *)manager identifier:(__kindof NSObject *)identifier theme:(__kindof NSObject *)theme shouldEnumeratorSubviews:(BOOL)shouldEnumeratorSubviews { + [super _qmui_themeDidChangeByManager:manager identifier:identifier theme:theme shouldEnumeratorSubviews:shouldEnumeratorSubviews]; + [self qmuiTheme_performUpdateInvocations]; +} + +- (void)qmuiTheme_performUpdateInvocations { + [[self.qmuiTheme_invocations allValues] enumerateObjectsUsingBlock:^(NSInvocation * _Nonnull invocation, NSUInteger idx, BOOL * _Nonnull stop) { + [invocation setTarget:self]; + [invocation invoke]; + }]; +} + + +- (NSMutableDictionary *)qmuiTheme_invocations { + NSMutableDictionary *qmuiTheme_invocations = objc_getAssociatedObject(self, _cmd); + if (!qmuiTheme_invocations) { + qmuiTheme_invocations = [NSMutableDictionary dictionary]; + objc_setAssociatedObject(self, _cmd, qmuiTheme_invocations, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return qmuiTheme_invocations; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.h b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.h new file mode 100644 index 00000000..099a4c39 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.h @@ -0,0 +1,61 @@ +/** + * 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. + */ +// +// UIColor+QMUITheme.h +// QMUIKit +// +// Created by MoLice on 2019/J/20. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIThemeManager; + +@interface UIColor (QMUITheme) + +/** + 生成一个动态的 color 对象,每次使用该颜色时都会动态根据当前的 QMUIThemeManager 主题返回对应的颜色。 + @param provider 当 color 被使用时,这个 provider 会被调用,返回对应当前主题的 color 值。请不要在这个 block 里做耗时操作。 + @return 当前主题下的实际色值,由 provider 返回 + */ ++ (UIColor *)qmui_colorWithThemeProvider:(UIColor *(^)(__kindof QMUIThemeManager *manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme))provider; + +/** + 生成一个动态的 color 对象,并以 name 为其标记,每次使用该颜色时都会动态根据当前的 QMUIThemeManager 主题返回对应的颜色。 + @param name 颜色的名称,默认为 nil + @param provider 当 color 被使用时,这个 provider 会被调用,返回对应当前主题的 color 值。请不要在这个 block 里做耗时操作。 + @return 当前主题下的实际色值,由 provider 返回 + */ ++ (UIColor *)qmui_colorWithName:(NSString * _Nullable)name + themeProvider:(UIColor *(^)(__kindof QMUIThemeManager *manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme))provider; + +/** + 生成一个动态的 color 对象,每次使用该颜色时都会动态根据当前的 managerName 和主题返回对应的颜色。 + @param managerName themeManager 的 name,用于区分不同维度的主题管理器 + @param provider 当 color 被使用时,这个 provider 会被调用,返回对应当前主题的 color 值。请不要在这个 block 里做耗时操作。 + @return 当前主题下的实际色值,由 provider 返回 + */ ++ (UIColor *)qmui_colorWithThemeManagerName:(__kindof NSObject *)managerName + provider:(UIColor *(^)(__kindof QMUIThemeManager *manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme))provider; + +/** + 生成一个动态的 color 对象,并以 name 为其标记,每次使用该颜色时都会动态根据当前的 managerName 和主题返回对应的颜色。 + @param name 颜色的名称,默认为 nil + @param managerName themeManager 的 name,用于区分不同维度的主题管理器 + @param provider 当 color 被使用时,这个 provider 会被调用,返回对应当前主题的 color 值。请不要在这个 block 里做耗时操作。 + @return 当前主题下的实际色值,由 provider 返回 + */ ++ (UIColor *)qmui_colorWithName:(NSString * _Nullable)name + themeManagerName:(__kindof NSObject *)managerName + provider:(UIColor *(^)(__kindof QMUIThemeManager *manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme))provider; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.m b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.m new file mode 100644 index 00000000..0be2e6df --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.m @@ -0,0 +1,194 @@ +/** + * 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. + */ +// +// UIColor+QMUITheme.m +// QMUIKit +// +// Created by MoLice on 2019/J/20. +// + +#import "UIColor+QMUITheme.h" +#import "QMUIThemeManager.h" +#import "QMUICore.h" +#import "NSMethodSignature+QMUI.h" +#import "UIColor+QMUI.h" +#import "QMUIThemePrivate.h" +#import "QMUIThemeManagerCenter.h" + +@implementation QMUIThemeColor + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // 随着 iOS 版本的迭代,需要不断检查 UIDynamicColor 对比 UIColor 多出来的方法是哪些,然后在 QMUIThemeColor 里补齐,否则可能出现”unrecognized selector sent to instance“的 crash + // https://github.com/Tencent/QMUI_iOS/issues/791 +#ifdef DEBUG + Class dynamicColorClass = NSClassFromString(@"UIDynamicColor"); + NSMutableSet *unrecognizedSelectors = NSMutableSet.new; + NSDictionary *> *methods = @{ + NSStringFromClass(UIColor.class): NSMutableSet.new, + NSStringFromClass(dynamicColorClass): NSMutableSet.new, + NSStringFromClass(self): NSMutableSet.new + }; + [methods enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull classString, NSMutableSet * _Nonnull methods, BOOL * _Nonnull stop) { + [NSObject qmui_enumrateInstanceMethodsOfClass:NSClassFromString(classString) includingInherited:NO usingBlock:^(Method _Nonnull method, SEL _Nonnull selector) { + [methods addObject:NSStringFromSelector(selector)]; + }]; + }]; + [methods[NSStringFromClass(UIColor.class)] enumerateObjectsUsingBlock:^(NSString * _Nonnull selectorString, BOOL * _Nonnull stop) { + if ([methods[NSStringFromClass(dynamicColorClass)] containsObject:selectorString]) { + [methods[NSStringFromClass(dynamicColorClass)] removeObject:selectorString]; + } + }]; + [methods[NSStringFromClass(dynamicColorClass)] enumerateObjectsUsingBlock:^(NSString * _Nonnull selectorString, BOOL * _Nonnull stop) { + if (![methods[NSStringFromClass(self)] containsObject:selectorString]) { + [unrecognizedSelectors addObject:selectorString]; + } + }]; + if (unrecognizedSelectors.count > 0) { + QMUILogWarn(NSStringFromClass(self), @"%@ 还需要实现以下方法:%@", NSStringFromClass(self), unrecognizedSelectors); + } +#endif + }); +} + +#pragma mark - Override + +- (void)set { + [self.qmui_rawColor set]; +} + +- (void)setFill { + [self.qmui_rawColor setFill]; +} + +- (void)setStroke { + [self.qmui_rawColor setStroke]; +} + +- (BOOL)getWhite:(CGFloat *)white alpha:(CGFloat *)alpha { + return [self.qmui_rawColor getWhite:white alpha:alpha]; +} + +- (BOOL)getHue:(CGFloat *)hue saturation:(CGFloat *)saturation brightness:(CGFloat *)brightness alpha:(CGFloat *)alpha { + return [self.qmui_rawColor getHue:hue saturation:saturation brightness:brightness alpha:alpha]; +} + +- (BOOL)getRed:(CGFloat *)red green:(CGFloat *)green blue:(CGFloat *)blue alpha:(CGFloat *)alpha { + return [self.qmui_rawColor getRed:red green:green blue:blue alpha:alpha]; +} + +- (UIColor *)colorWithAlphaComponent:(CGFloat)alpha { + return [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return [self.themeProvider(manager, identifier, theme) colorWithAlphaComponent:alpha]; + }]; +} + +- (CGFloat)alphaComponent { + return self.qmui_rawColor.qmui_alpha; +} + +- (CGColorRef)CGColor { + // 这个 UIColor 对象,以前是直接拿 self.qmui_rawColor,但某些场景(具体是什么场景不知道了,看 git commit 是 2019 年的提交)这样有问题,所以才改为先用 self.qmui_rawColor.CGColor 生成一个 UIColor。 + UIColor *rawColor = [UIColor colorWithCGColor:self.qmui_rawColor.CGColor]; + + // CGColor 必须通过 CGColorCreate 创建。UIColor.CGColor 返回的是一个多对象复用的 CGColor 值(例如,如果 QMUIThemeA.light 值和 UIColorB 的值刚好相同,那么他们的 CGColor 可能也是同一个对象,所以 UIColorB.CGColor 可能会错误地使用了原本仅属于 QMUIThemeColorA 的 bindObject) + // 经测试,qmui_red 系列接口适用于不同的 ColorSpace,应该是能放心使用的😜 + // https://github.com/Tencent/QMUI_iOS/issues/1463 + CGColorSpaceRef spaceRef = CGColorSpaceCreateDeviceRGB(); + CGColorRef cgColor = CGColorCreate(spaceRef, (CGFloat[]){rawColor.qmui_red, rawColor.qmui_green, rawColor.qmui_blue, rawColor.qmui_alpha}); + CGColorSpaceRelease(spaceRef); + + [(__bridge id)(cgColor) qmui_bindObject:self forKey:QMUICGColorOriginalColorBindKey]; + return (CGColorRef)CFAutorelease(cgColor); +} + +- (NSString *)colorSpaceName { + return [((QMUIThemeColor *)self.qmui_rawColor) colorSpaceName]; +} + +- (id)copyWithZone:(NSZone *)zone { + QMUIThemeColor *color = [[[self class] allocWithZone:zone] init]; + color.name = self.name; + color.managerName = self.managerName; + color.themeProvider = self.themeProvider; + return color; +} + +- (BOOL)isEqual:(id)object { + return self == object;// 例如在 UIView setTintColor: 时会比较两个 color 是否相等,如果相等,则不会触发 tintColor 的更新。由于 dynamicColor 实际的返回色值随时可能变化,所以即便当前的 qmui_rawColor 值相等,也不应该认为两个 dynamicColor 相等(有可能 themeProvider block 内的逻辑不一致,只是其中的某个条件下 return 的 qmui_rawColor 恰好相同而已),所以这里直接返回 NO。 +} + +- (NSUInteger)hash { + return (NSUInteger)self.themeProvider;// 与 UIDynamicProviderColor 相同 +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@,%@qmui_rawColor = %@", [super description], self.name.length ? [NSString stringWithFormat:@" name = %@, ", self.name] : @" ", self.qmui_rawColor]; +} + +- (UIColor *)_highContrastDynamicColor { + return self; +} + +- (UIColor *)_resolvedColorWithTraitCollection:(UITraitCollection *)traitCollection { + return self.qmui_rawColor; +} + +#pragma mark - + +@dynamic qmui_isDynamicColor; + +- (NSString *)qmui_name { + return self.name; +} + +- (UIColor *)qmui_rawColor { + QMUIThemeManager *manager = [QMUIThemeManagerCenter themeManagerWithName:self.managerName]; + UIColor *color = self.themeProvider(manager, manager.currentThemeIdentifier, manager.currentTheme); + UIColor *result = color.qmui_rawColor; + return result; +} + +- (BOOL)qmui_isQMUIDynamicColor { + return YES; +} + +// _isDynamic 是系统私有的方法,实现它有两个作用: +// 1. 在某些方法里(例如 UIView.backgroundColor),系统会判断当前的 color 是否为 _isDynamic,如果是,则返回 color 本身,如果否,则返回 color 的 CGColor,因此如果 QMUIThemeColor 不实现 _isDynamic 的话,`a.backgroundColor = b.backgroundColor`这种写法就会出错,因为从 `b.backgroundColor` 获取到的 color 已经是用 CGColor 重新创建的系统 UIColor,而非 QMUIThemeColor 了。 +// 2. 当 iOS 13 系统设置里的 Dark Mode 发生切换时,系统会自动刷新带有 _isDynamic 方法的 color 对象,当然这个对 QMUI 而言作用不大,因为 QMUIThemeManager 有自己一套刷新逻辑,且很少有人会用 QMUIThemeColor 但却只依赖于 iOS 13 系统来刷新界面。 +// 注意,QMUIThemeColor 是 UIColor 的直接子类,只有这种关系才能这样直接定义并重写,不能在 UIColor Category 里定义,否则可能污染 UIDynamicColor 里的 _isDynamic 的实现 +- (BOOL)_isDynamic { + return !!self.themeProvider; +} + +@end + +@implementation UIColor (QMUITheme) + ++ (instancetype)qmui_colorWithThemeProvider:(UIColor * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject * _Nullable, __kindof NSObject * _Nullable))provider { + return [self qmui_colorWithName:nil themeManagerName:QMUIThemeManagerNameDefault provider:provider]; +} + ++ (UIColor *)qmui_colorWithName:(NSString *)name themeProvider:(UIColor * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject * _Nullable, __kindof NSObject * _Nullable))provider { + return [self qmui_colorWithName:name themeManagerName:QMUIThemeManagerNameDefault provider:provider]; +} + ++ (UIColor *)qmui_colorWithThemeManagerName:(__kindof NSObject *)managerName provider:(UIColor * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject * _Nullable, __kindof NSObject * _Nullable))provider { + return [self qmui_colorWithName:nil themeManagerName:managerName provider:provider]; +} + ++ (UIColor *)qmui_colorWithName:(NSString *)name themeManagerName:(__kindof NSObject *)managerName provider:(UIColor * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject * _Nullable, __kindof NSObject * _Nullable))provider { + QMUIThemeColor *color = QMUIThemeColor.new; + color.name = name; + color.managerName = managerName; + color.themeProvider = provider; + return color; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIImage+QMUITheme.h b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIImage+QMUITheme.h new file mode 100644 index 00000000..af9eeaf6 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIImage+QMUITheme.h @@ -0,0 +1,78 @@ +/** + * 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. + */ +// +// UIImage+QMUITheme.h +// QMUIKit +// +// Created by MoLice on 2019/J/16. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIThemeManager; + +@protocol QMUIDynamicImageProtocol + +@required + +/// 获取当前 UIImage 的实际图片(返回的图片必定不是 dynamic image) +@property(nonatomic, strong, readonly) UIImage *qmui_rawImage; + +/// 标志当前 UIImage 对象是否为动态图片(由 [UIImage qmui_imageWithThemeProvider:] 创建的颜色 +@property(nonatomic, assign, readonly) BOOL qmui_isDynamicImage; + +@end + +@interface UIImage (QMUITheme) + +/** + 生成一个动态的 image 对象,每次使用该图片时都会动态根据当前的 QMUIThemeManager 主题返回对应的图片。 + @param provider 当 image 被使用时,这个 provider 会被调用,返回对应当前主题的 image 值 + @return 当前主题下的实际图片,由 provider 返回 + */ ++ (UIImage *)qmui_imageWithThemeProvider:(UIImage *(^)(__kindof QMUIThemeManager *manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme))provider; + +/** + 生成一个动态的 image 对象,并以 name 为其标记,每次使用该图片时都会动态根据当前的 QMUIThemeManager 主题返回对应的图片。 + @param name 动态 image 的名称,默认为 nil + @param provider 当 image 被使用时,这个 provider 会被调用,返回对应当前主题的 image 值 + @return 当前主题下的实际图片,由 provider 返回 +*/ ++ (UIImage *)qmui_imageWithName:(NSString * _Nullable)name + themeProvider:(UIImage *(^)(__kindof QMUIThemeManager *manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme))provider; + +/** + 生成一个动态的 image 对象,每次使用该图片时都会动态根据当前的 QMUIThemeManager name 和主题返回对应的图片。 + @param managerName themeManager 的 name,用于区分不同维度的主题管理器 + @param provider 当 image 被使用时,这个 provider 会被调用,返回对应当前主题的 image 值 + @return 当前主题下的实际图片,由 provider 返回 +*/ ++ (UIImage *)qmui_imageWithThemeManagerName:(__kindof NSObject *)managerName + provider:(UIImage *(^)(__kindof QMUIThemeManager *manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme))provider; + +/** + 生成一个动态的 image 对象,并以 name 为其标记,每次使用该图片时都会动态根据当前的 QMUIThemeManager name 和主题返回对应的图片。 + @param name 动态 image 的名称,默认为 nil + @param managerName themeManager 的 name,用于区分不同维度的主题管理器 + @param provider 当 image 被使用时,这个 provider 会被调用,返回对应当前主题的 image 值 + @return 当前主题下的实际图片,由 provider 返回 +*/ ++ (UIImage *)qmui_imageWithName:(NSString * _Nullable)name + themeManagerName:(__kindof NSObject *)managerName + provider:(UIImage *(^)(__kindof QMUIThemeManager *manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme))provider; + +/** + 内部用,标志 QMUIThemeImage 对 UIImage (QMUI) 里使用动态颜色生成动态图片的适配 hook 是否已生效。例如在配置表这种“加载时机特别早”的场景,此时 UIImage (QMUITheme) +load 方法尚未被调用,这些 hook 还没生效,此时如果你使用 [UIImage qmui_imageWithTintColor:dynamicColor] 得到的 image 是无法自动响应 theme 切换的。 + */ +@property(class, nonatomic, assign, readonly) BOOL qmui_generatorSupportsDynamicColor; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIImage+QMUITheme.m b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIImage+QMUITheme.m new file mode 100644 index 00000000..35dffade --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIImage+QMUITheme.m @@ -0,0 +1,559 @@ +/** + * 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. + */ +// +// UIImage+QMUITheme.m +// QMUIKit +// +// Created by MoLice on 2019/J/16. +// + +#import "UIImage+QMUITheme.h" +#import "QMUIThemeManager.h" +#import "QMUIThemeManagerCenter.h" +#import "QMUIThemePrivate.h" +#import "NSMethodSignature+QMUI.h" +#import "QMUICore.h" +#import "UIImage+QMUI.h" +#import + +@interface UIImage () + +@property(nonatomic, assign) BOOL qmui_shouldUseSystemIMP; ++ (nullable UIImage *)qmui_dynamicImageWithOriginalImage:(UIImage *)image tintColor:(UIColor *)tintColor originalActionBlock:(UIImage * (^)(UIImage *aImage, UIColor *aTintColor))originalActionBlock; +@end + +@interface QMUIThemeImageCache : NSCache + +@end + +@implementation QMUIThemeImageCache + +- (instancetype)init { + if (self = [super init]) { + // NSCache 在 app 进入后台时会删除所有缓存,它的实现方式是在 init 的时候去监听 UIApplicationDidEnterBackgroundNotification ,一旦进入后台则调用 removeAllObjects,通过 removeObserver 可以禁用掉这个策略 + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; + } + return self; +} + +@end + +@interface QMUIAvoidExceptionProxy : NSProxy +@end + +@implementation QMUIAvoidExceptionProxy + ++ (instancetype)proxy { + static dispatch_once_t onceToken; + static QMUIAvoidExceptionProxy *instance = nil; + dispatch_once(&onceToken,^{ + instance = [super alloc]; + }); + return instance; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { + return [NSMethodSignature qmui_avoidExceptionSignature]; +} + +@end + +@interface QMUIThemeImage() + +@property(nonatomic, strong) QMUIThemeImageCache *cachedRawImages; + +@end + +@implementation QMUIThemeImage + +static IMP qmui_getMsgForwardIMP(NSObject *self, SEL selector) { + + IMP msgForwardIMP = _objc_msgForward; +#if !defined(__arm64__) + // As an ugly internal runtime implementation detail in the 32bit runtime, we need to determine of the method we hook returns a struct or anything larger than id. + // https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html + // https://github.com/ReactiveCocoa/ReactiveCocoa/issues/783 + // http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf (Section 5.4) + Method method = class_getInstanceMethod(self.class, selector); + const char *encoding = method_getTypeEncoding(method); + BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B; + if (methodReturnsStructValue) { + @try { + // 以下代码参考 JSPatch 的实现,但在 OpenCV 时会抛异常 + NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:encoding]; + if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location == NSNotFound) { + methodReturnsStructValue = NO; + } + } @catch (__unused NSException *e) { + // 以下代码参考 Aspect 的实现,可以兼容 OpenCV + @try { + NSUInteger valueSize = 0; + NSGetSizeAndAlignment(encoding, &valueSize, NULL); + + if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) { + methodReturnsStructValue = NO; + } + } @catch (NSException *exception) {} + } + } + if (methodReturnsStructValue) { + msgForwardIMP = (IMP)_objc_msgForward_stret; + } +#endif + return msgForwardIMP; +} + +- (void)dealloc { + _themeProvider = nil; +} + +- (id)forwardingTargetForSelector:(SEL)aSelector { + if (self.qmui_rawImage) { + // 这里不能加上 [self.qmui_rawImage respondsToSelector:aSelector] 的判断,否则 UIImage 没有机会做消息转发 + return self.qmui_rawImage; + } + // 在 dealloc 的时候 UIImage 会调用 _isNamed 是用于判断 image 对象是否由 [UIImage imageNamed:] 创建的,并根据这个结果决定是否缓存 image,但是 QMUIThemeImage 仅仅是一个容器,真正的缓存工作会在 qmui_rawImage 的 dealloc 执行,所以可以忽略这个方法的调用 + NSArray *ignoreSelectorNames = @[@"_isNamed"]; + if (![ignoreSelectorNames containsObject:NSStringFromSelector(aSelector)]) { + QMUILogWarn(@"UIImage+QMUITheme", @"QMUIThemeImage 试图执行 %@ 方法,但是 qmui_rawImage 为 nil", NSStringFromSelector(aSelector)); + } + return [QMUIAvoidExceptionProxy proxy]; +} + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + Class selfClass = [QMUIThemeImage class]; + UIImage *instance = UIImage.new; + // QMUIThemeImage 覆盖重写了大部分 UIImage 的方法,在这些方法调用时,会交给 qmui_rawImage 处理 + // 除此之外 UIImage 内部还有很多私有方法,无法全部在 QMUIThemeImage 重写一遍,这些方法将通过消息转发的形式交给 qmui_rawImage 调用。 + [NSObject qmui_enumrateInstanceMethodsOfClass:instance.class includingInherited:NO usingBlock:^(Method _Nonnull method, SEL _Nonnull selector) { + // 如果 QMUIThemeImage 已经实现了该方法,则不需要消息转发 + if (class_getInstanceMethod(selfClass, selector) != method) return; + const char * typeDescription = (char *)method_getTypeEncoding(method); + class_addMethod(selfClass, selector, qmui_getMsgForwardIMP(instance, selector), typeDescription); + }]; + }); +} + +// 让 QMUIThemeImage 支持 NSCopying 是为了修复 iOS 12 及以下版本,QMUIThemeImage 在搭配 resizable 使用的情况下可能无法跟随主题刷新的 bug,使用的地方在 UIView+QMUITheme qmui_themeDidChangeByManager:identifier:theme 内。 +// https://github.com/Tencent/QMUI_iOS/issues/971 +- (id)copyWithZone:(NSZone *)zone { + QMUIThemeImage *image = [[self.class allocWithZone:zone] init]; + image.cachedRawImages = self.cachedRawImages; + image.name = self.name; + image.managerName = self.managerName; + image.themeProvider = self.themeProvider; + return image; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p>,%@rawImage is %@", NSStringFromClass(self.class), self, self.name.length ? [NSString stringWithFormat:@" name = %@, ", self.name] : @" ", self.qmui_rawImage.description]; +} + +- (instancetype)init { + return ((id (*)(id, SEL))[NSObject instanceMethodForSelector:_cmd])(self, _cmd); +} + +- (NSString *)qmui_name { + return self.name; +} + +- (BOOL)respondsToSelector:(SEL)aSelector { + if ([super respondsToSelector:aSelector]) { + return YES; + } + + return [self.qmui_rawImage respondsToSelector:aSelector]; +} + +- (BOOL)isKindOfClass:(Class)aClass { + if (aClass == QMUIThemeImage.class) return YES; + return [self.qmui_rawImage isKindOfClass:aClass]; +} + +- (BOOL)isMemberOfClass:(Class)aClass { + if (aClass == QMUIThemeImage.class) return YES; + return [self.qmui_rawImage isMemberOfClass:aClass]; +} + +- (BOOL)conformsToProtocol:(Protocol *)aProtocol { + return [self.qmui_rawImage conformsToProtocol:aProtocol]; +} + +- (NSUInteger)hash { + return (NSUInteger)self.themeProvider; +} + +- (BOOL)isEqual:(id)object { + return NO; +} + +- (CGSize)size { + return self.qmui_rawImage.size; +} + +- (CGImageRef)CGImage { + return self.qmui_rawImage.CGImage; +} + +- (CIImage *)CIImage { + return self.qmui_rawImage.CIImage; +} + +- (UIImageOrientation)imageOrientation { + return self.qmui_rawImage.imageOrientation; +} + +- (CGFloat)scale { + return self.qmui_rawImage.scale; +} + +- (NSArray *)images { + return self.qmui_rawImage.images; +} + +- (NSTimeInterval)duration { + return self.qmui_rawImage.duration; +} + +- (UIEdgeInsets)alignmentRectInsets { + return self.qmui_rawImage.alignmentRectInsets; +} + +- (void)drawAtPoint:(CGPoint)point { + [self.qmui_rawImage drawAtPoint:point]; +} + +- (void)drawAtPoint:(CGPoint)point blendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha { + [self.qmui_rawImage drawAtPoint:point blendMode:blendMode alpha:alpha]; +} + +- (void)drawInRect:(CGRect)rect { + [self.qmui_rawImage drawInRect:rect]; +} + +- (void)drawInRect:(CGRect)rect blendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha { + [self.qmui_rawImage drawInRect:rect blendMode:blendMode alpha:alpha]; +} + +- (void)drawAsPatternInRect:(CGRect)rect { + [self.qmui_rawImage drawAsPatternInRect:rect]; +} + +- (UIImage *)resizableImageWithCapInsets:(UIEdgeInsets)capInsets { + return [UIImage qmui_imageWithThemeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return [self.qmui_rawImage resizableImageWithCapInsets:capInsets]; + }]; +} + +- (UIImage *)resizableImageWithCapInsets:(UIEdgeInsets)capInsets resizingMode:(UIImageResizingMode)resizingMode { + return [UIImage qmui_imageWithThemeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return [self.qmui_rawImage resizableImageWithCapInsets:capInsets resizingMode:resizingMode]; + }]; +} + +- (UIEdgeInsets)capInsets { + return [self.qmui_rawImage capInsets]; +} + +- (UIImageResizingMode)resizingMode { + return [self.qmui_rawImage resizingMode]; +} + +- (UIImage *)imageWithAlignmentRectInsets:(UIEdgeInsets)alignmentInsets { + return [self.qmui_rawImage imageWithAlignmentRectInsets:alignmentInsets]; +} + +- (UIImage *)imageWithRenderingMode:(UIImageRenderingMode)renderingMode { + return [self.qmui_rawImage imageWithRenderingMode:renderingMode]; +} + +- (UIImageRenderingMode)renderingMode { + return self.qmui_rawImage.renderingMode; +} + +- (UIGraphicsImageRendererFormat *)imageRendererFormat { + return self.qmui_rawImage.imageRendererFormat; +} + +- (UITraitCollection *)traitCollection { + return self.qmui_rawImage.traitCollection; +} + +- (UIImageAsset *)imageAsset { + return self.qmui_rawImage.imageAsset; +} + +- (UIImage *)imageFlippedForRightToLeftLayoutDirection { + return self.qmui_rawImage.imageFlippedForRightToLeftLayoutDirection; +} + +- (BOOL)flipsForRightToLeftLayoutDirection { + return self.qmui_rawImage.flipsForRightToLeftLayoutDirection; +} + +- (UIImage *)imageWithHorizontallyFlippedOrientation { + return self.qmui_rawImage.imageWithHorizontallyFlippedOrientation; +} + +- (BOOL)isSymbolImage { + return self.qmui_rawImage.isSymbolImage; +} + +- (CGFloat)baselineOffsetFromBottom { + return self.qmui_rawImage.baselineOffsetFromBottom; +} + +- (BOOL)hasBaseline { + return self.qmui_rawImage.hasBaseline; +} + +- (UIImage *)imageWithBaselineOffsetFromBottom:(CGFloat)baselineOffset { + return [self.qmui_rawImage imageWithBaselineOffsetFromBottom:baselineOffset]; +} + +- (UIImage *)imageWithoutBaseline { + return self.qmui_rawImage.imageWithoutBaseline; +} + +- (UIImageConfiguration *)configuration { + return self.qmui_rawImage.configuration; +} + +- (UIImage *)imageWithConfiguration:(UIImageConfiguration *)configuration { + return [self.qmui_rawImage imageWithConfiguration:configuration]; +} + +- (UIImageSymbolConfiguration *)symbolConfiguration { + return self.qmui_rawImage.symbolConfiguration; +} + +- (UIImage *)imageByApplyingSymbolConfiguration:(UIImageSymbolConfiguration *)configuration { + return [self.qmui_rawImage imageByApplyingSymbolConfiguration:configuration]; +} + +#pragma mark - + +- (UIImage *)qmui_rawImage { + if (!_themeProvider) return nil; + QMUIThemeManager *manager = [QMUIThemeManagerCenter themeManagerWithName:self.managerName]; + NSString *cacheKey = [NSString stringWithFormat:@"%@%@_%@", self.name ? [NSString stringWithFormat:@"%@_", self.name] : @"", manager.name, manager.currentThemeIdentifier]; + UIImage *rawImage = [self.cachedRawImages objectForKey:cacheKey]; + if (!rawImage) { + rawImage = self.themeProvider(manager, manager.currentThemeIdentifier, manager.currentTheme).qmui_rawImage; + if (rawImage) [self.cachedRawImages setObject:rawImage forKey:cacheKey]; + } + return rawImage; +} + +- (BOOL)qmui_isDynamicImage { + return YES; +} + +#pragma mark - Translator + +// 由于 QMUIThemeImage 的实现里,如果某些方法 QMUIThemeImage 本身没实现,那么就会以消息转发的方式转发给 rawImage,这就导致我们无法直接用 method swizzle 的方式去重写 UIImage.class 的 imageWithTintColor 系列方法并期望它能同时作用于 UIImage 和 QMUIThemeImage(后者总是无效的,因为最终接收消息的总是 rawImage 而不是 QMUIThemeImage),所以这里需要这么冗余地显式写一遍 + +- (UIImage *)imageWithTintColor:(UIColor *)color { + return [UIImage qmui_dynamicImageWithOriginalImage:self tintColor:color originalActionBlock:^UIImage *(UIImage *aImage, UIColor *aTintColor) { + aImage.qmui_shouldUseSystemIMP = YES; + return [aImage imageWithTintColor:color]; + }]; +} + +- (UIImage *)imageWithTintColor:(UIColor *)color renderingMode:(UIImageRenderingMode)renderingMode { + return [UIImage qmui_dynamicImageWithOriginalImage:self tintColor:color originalActionBlock:^UIImage *(UIImage *aImage, UIColor *aTintColor) { + aImage.qmui_shouldUseSystemIMP = YES; + return [aImage imageWithTintColor:color renderingMode:renderingMode]; + }]; +} + +- (UIImage *)qmui_imageWithTintColor:(UIColor *)color { + return [UIImage qmui_dynamicImageWithOriginalImage:self tintColor:color originalActionBlock:^UIImage *(UIImage *aImage, UIColor *aTintColor) { + aImage.qmui_shouldUseSystemIMP = YES; + return [aImage qmui_imageWithTintColor:color]; + }]; +} + +// QMUIThemeImage 一定不存在 qmui_shouldUseSystemIMP 方法,该方法是为 UIImage 提供的,所以这里强制返回 NO。不这么处理的话,当遇到 [QMUIThemeImage qmui_imageWithTintColor:QMUIThemeColor] 时,该图片会无效。 +- (BOOL)qmui_shouldUseSystemIMP { + return NO; +} + +@end + +@implementation UIImage (QMUITheme) + +QMUISynthesizeBOOLProperty(qmui_shouldUseSystemIMP, setQmui_shouldUseSystemIMP) + +static BOOL generatorSupportsDynamicColor = NO; ++ (BOOL)qmui_generatorSupportsDynamicColor { + return generatorSupportsDynamicColor; +} + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // 支持用一个动态颜色直接生成一个动态图片 + OverrideImplementation(object_getClass(UIImage.class), @selector(qmui_imageWithColor:size:cornerRadius:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UIImage *selfObject, UIColor *color, CGSize size, CGFloat cornerRadius) { + + // call super + UIImage * (^callSuperBlock)(UIColor *, CGSize, CGFloat) = ^UIImage *(UIColor *aColor, CGSize aSize, CGFloat aCornerRadius) { + UIImage * (*originSelectorIMP)(id, SEL, UIColor *, CGSize, CGFloat); + originSelectorIMP = (UIImage * (*)(id, SEL, UIColor *, CGSize, CGFloat))originalIMPProvider(); + UIImage * result = originSelectorIMP(selfObject, originCMD, aColor, aSize, aCornerRadius); + return result; + }; + + if ([color isKindOfClass:QMUIThemeColor.class]) { + return [UIImage qmui_imageWithThemeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return callSuperBlock(((QMUIThemeColor *)color).themeProvider(manager, identifier, theme), size, cornerRadius); + }]; + } + return callSuperBlock(color, size, cornerRadius); + }; + }); + + OverrideImplementation(object_getClass(UIImage.class), @selector(qmui_imageWithColor:size:cornerRadiusArray:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UIImage *selfObject, UIColor *color, CGSize size, NSArray *cornerRadius) { + + // call super + UIImage * (^callSuperBlock)(UIColor *, CGSize, NSArray *) = ^UIImage *(UIColor *aColor, CGSize aSize, NSArray * aCornerRadius) { + UIImage * (*originSelectorIMP)(id, SEL, UIColor *, CGSize, NSArray *); + originSelectorIMP = (UIImage * (*)(id, SEL, UIColor *, CGSize, NSArray *))originalIMPProvider(); + UIImage * result = originSelectorIMP(selfObject, originCMD, aColor, aSize, aCornerRadius); + return result; + }; + + if ([color isKindOfClass:QMUIThemeColor.class]) { + return [UIImage qmui_imageWithThemeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return callSuperBlock(((QMUIThemeColor *)color).themeProvider(manager, identifier, theme), size, cornerRadius); + }]; + } + return callSuperBlock(color, size, cornerRadius); + }; + }); + + // 令一个静态图片叠加动态颜色可以转换成动态图片 + OverrideImplementation([UIImage class], @selector(qmui_imageWithTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UIImage *selfObject, UIColor *tintColor) { + + UIImage *result = [UIImage qmui_dynamicImageWithOriginalImage:selfObject tintColor:tintColor originalActionBlock:^UIImage *(UIImage *aImage, UIColor *aTintColor) { + aImage.qmui_shouldUseSystemIMP = YES; + return [aImage qmui_imageWithTintColor:aTintColor]; + }]; + if (!result) { + // call super + UIImage *(*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (UIImage * (*)(id, SEL, UIColor *))originalIMPProvider(); + result = originSelectorIMP(selfObject, originCMD, tintColor); + } + return result; + }; + }); + + // 如果一个静态的 UIImage 通过 imageWithTintColor: 传入一个动态的颜色,那么这个 UIImage 也会变成动态的,但这个动态图片是 iOS 13 系统原生的动态图片,无法响应 QMUITheme,所以这里需要为 QMUIThemeImage 做特殊处理。 + // 注意,系统的 imageWithTintColor: 不会调用 imageWithTintColor:renderingMode:,所以要分开重写两个方法 + OverrideImplementation([UIImage class], @selector(imageWithTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UIImage *selfObject, UIColor *tintColor) { + + UIImage *result = [UIImage qmui_dynamicImageWithOriginalImage:selfObject tintColor:tintColor originalActionBlock:^UIImage *(UIImage *aImage, UIColor *aTintColor) { + aImage.qmui_shouldUseSystemIMP = YES; + return [aImage imageWithTintColor:aTintColor]; + }]; + if (!result) { + // call super + UIImage *(*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (UIImage * (*)(id, SEL, UIColor *))originalIMPProvider(); + result = originSelectorIMP(selfObject, originCMD, tintColor); + } + return result; + }; + }); + OverrideImplementation([UIImage class], @selector(imageWithTintColor:renderingMode:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UIImage *selfObject, UIColor *tintColor, UIImageRenderingMode renderingMode) { + + UIImage *result = [UIImage qmui_dynamicImageWithOriginalImage:selfObject tintColor:tintColor originalActionBlock:^UIImage *(UIImage *aImage, UIColor *aTintColor) { + aImage.qmui_shouldUseSystemIMP = YES; + return [aImage imageWithTintColor:aTintColor renderingMode:renderingMode]; + }]; + if (!result) { + // call super + UIImage *(*originSelectorIMP)(id, SEL, UIColor *, UIImageRenderingMode); + originSelectorIMP = (UIImage * (*)(id, SEL, UIColor *, UIImageRenderingMode))originalIMPProvider(); + result = originSelectorIMP(selfObject, originCMD, tintColor, renderingMode); + } + return result; + }; + }); + + generatorSupportsDynamicColor = YES; + }); +} + ++ (UIImage *)qmui_imageWithThemeProvider:(UIImage * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject * _Nullable, __kindof NSObject * _Nullable))provider { + return [self qmui_imageWithName:nil themeManagerName:QMUIThemeManagerNameDefault provider:provider]; +} + ++ (UIImage *)qmui_imageWithName:(NSString *)name themeProvider:(UIImage * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject * _Nullable, __kindof NSObject * _Nullable))provider { + return [self qmui_imageWithName:name themeManagerName:QMUIThemeManagerNameDefault provider:provider]; +} + ++ (UIImage *)qmui_imageWithThemeManagerName:(__kindof NSObject *)managerName provider:(UIImage * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject * _Nullable, __kindof NSObject * _Nullable))provider { + return [self qmui_imageWithName:nil themeManagerName:managerName provider:provider]; +} + ++ (UIImage *)qmui_imageWithName:(NSString *)name themeManagerName:(__kindof NSObject *)managerName provider:(UIImage * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject * _Nullable, __kindof NSObject * _Nullable))provider { + QMUIThemeImage *image = [[QMUIThemeImage alloc] init]; + image.cachedRawImages = [[QMUIThemeImageCache alloc] init]; + image.name = name; + image.managerName = managerName; + image.themeProvider = provider; + return (UIImage *)image; +} + ++ (nullable UIImage *)qmui_dynamicImageWithOriginalImage:(UIImage *)image tintColor:(UIColor *)tintColor originalActionBlock:(UIImage * (^)(UIImage *aImage, UIColor *aTintColor))originalActionBlock { + if (image.qmui_shouldUseSystemIMP) { + image.qmui_shouldUseSystemIMP = NO; + return nil; + } + if ([image isKindOfClass:QMUIThemeImage.class]) { + // 当前是动态 image,不管 tintColor 是否为动态的,都返回一个动态 image + QMUIThemeImage *themeImage = (QMUIThemeImage *)image; + return [UIImage qmui_imageWithThemeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return originalActionBlock(themeImage.themeProvider(manager, identifier, theme), tintColor); + }]; + } + if ([tintColor isKindOfClass:QMUIThemeColor.class]) { + // 当前是静态 image,则只有当 tintColor 是动态的时候才将静态 image 转换为动态 image + return [UIImage qmui_imageWithThemeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + QMUIThemeColor *themeColor = (QMUIThemeColor *)tintColor; + return originalActionBlock(image, themeColor.themeProvider(manager, identifier, theme)); + }]; + } + + return nil; +} + +#pragma mark - + +- (UIImage *)qmui_rawImage { + return self; +} + +- (BOOL)qmui_isDynamicImage { + return NO; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.h b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.h new file mode 100644 index 00000000..02cf19db --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.h @@ -0,0 +1,47 @@ +/** + * 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. + */ +// +// UIView+QMUITheme.h +// QMUIKit +// +// Created by MoLice on 2019/6/21. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIThemeManager; + +@interface UIView (QMUITheme) + +/** + 注册当前 view 里需要在主题变化时被重新设置的 property,当主题变化时,会通过 qmui_themeDidChangeByManager:identifier:theme: 来重新调用一次 self.xxx = xxx,以达到刷新界面的目的。 + @param getters 属性的 getter, 内部会根据命名规则自动转换得到 setter,再通过 performSelector 的形式调用 getter 和 setter + */ +- (void)qmui_registerThemeColorProperties:(NSArray *)getters; + +/** + 注销通过 qmui_registerThemeColorProperties: 注册的 property + @param getters 属性的 getter, 内部会根据命名规则自动转换得到 setter,再通过 performSelector 的形式调用 getter 和 setter + */ +- (void)qmui_unregisterThemeColorProperties:(NSArray *)getters; + +/** + 当主题变化时这个方法会被调用,通过 registerThemeColorProperties: 方法注册的属性也会在这里被更新(所以记得要调用 super)。registerThemeColorProperties: 无法满足的需求可以重写这个方法自行实现。 + @param manager 当前的主题管理对象 + @param identifier 当前主题的标志,可自行修改参数类型为目标类型 + @param theme 当前主题对象,可自行修改参数类型为目标类型 + */ +- (void)qmui_themeDidChangeByManager:(nullable QMUIThemeManager *)manager identifier:(nullable __kindof NSObject *)identifier theme:(nullable __kindof NSObject *)theme NS_REQUIRES_SUPER; + +@property(nonatomic, copy, nullable) void (^qmui_themeDidChangeBlock)(void); + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m new file mode 100644 index 00000000..32f91c15 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m @@ -0,0 +1,226 @@ +/** + * 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. + */ +// +// UIView+QMUITheme.m +// QMUIKit +// +// Created by MoLice on 2019/6/21. +// + +#import "UIView+QMUITheme.h" +#import "QMUICore.h" +#import "UIView+QMUI.h" +#import "UIColor+QMUI.h" +#import "UIImage+QMUI.h" +#import "UIImage+QMUITheme.h" +#import "UIVisualEffect+QMUITheme.h" +#import "QMUIThemeManagerCenter.h" +#import "CALayer+QMUI.h" +#import "QMUIThemeManager.h" +#import "QMUIThemePrivate.h" +#import "NSObject+QMUI.h" +#import "UITextInputTraits+QMUI.h" + +@implementation UIView (QMUITheme) + +QMUISynthesizeIdCopyProperty(qmui_themeDidChangeBlock, setQmui_themeDidChangeBlock) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + OverrideImplementation([UIView class], @selector(setHidden:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, BOOL firstArgv) { + + BOOL valueChanged = selfObject.hidden != firstArgv; + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if (valueChanged) { + // UIView.qmui_currentThemeIdentifier 只是为了实现判断当前的 theme 是否有发生变化,所以可以构造成一个 string,但怎么避免每次 hidden 切换时都要遍历所有的 subviews? + [selfObject _qmui_themeDidChangeByManager:nil identifier:nil theme:nil shouldEnumeratorSubviews:YES]; + } + }; + }); + + OverrideImplementation([UIView class], @selector(setAlpha:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, CGFloat firstArgv) { + + BOOL willShow = selfObject.alpha <= 0 && firstArgv > 0.01; + + // call super + void (*originSelectorIMP)(id, SEL, CGFloat); + originSelectorIMP = (void (*)(id, SEL, CGFloat))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if (willShow) { + // 只设置 identifier 就可以了,内部自然会去同步更新 theme + [selfObject _qmui_themeDidChangeByManager:nil identifier:nil theme:nil shouldEnumeratorSubviews:YES]; + } + }; + }); + + // 这几个 class 实现了自己的 didMoveToWindow 且没有调用 super,所以需要每个都替换一遍方法 + NSArray *classes = @[UIView.class, + UICollectionView.class, + UITextField.class, + UISearchBar.class, + NSClassFromString(@"UITableViewLabel")]; + if (NSClassFromString(@"WKWebView")) { + classes = [classes arrayByAddingObject:NSClassFromString(@"WKWebView")]; + } + [classes enumerateObjectsUsingBlock:^(Class _Nonnull class, NSUInteger idx, BOOL * _Nonnull stop) { + ExtendImplementationOfVoidMethodWithoutArguments(class, @selector(didMoveToWindow), ^(UIView *selfObject) { + // enumerateSubviews 为 NO 是因为当某个 view 的 didMoveToWindow 被触发时,它的每个 subview 的 didMoveToWindow 也都会被触发,所以不需要遍历 subview 了 + if (selfObject.window) { + [selfObject _qmui_themeDidChangeByManager:nil identifier:nil theme:nil shouldEnumeratorSubviews:NO]; + } + }); + }]; + }); +} + +- (void)qmui_registerThemeColorProperties:(NSArray *)getters { + [getters enumerateObjectsUsingBlock:^(NSString * _Nonnull getterString, NSUInteger idx, BOOL * _Nonnull stop) { + SEL getter = NSSelectorFromString(getterString); + SEL setter = setterWithGetter(getter); + NSString *setterString = NSStringFromSelector(setter); + QMUIAssert([self respondsToSelector:getter], @"UIView (QMUITheme)", @"register theme color fails, %@ does not have method called %@", NSStringFromClass(self.class), getterString); + QMUIAssert([self respondsToSelector:setter], @"UIView (QMUITheme)", @"register theme color fails, %@ does not have method called %@", NSStringFromClass(self.class), setterString); + + if (!self.qmuiTheme_themeColorProperties) { + self.qmuiTheme_themeColorProperties = NSMutableDictionary.new; + } + self.qmuiTheme_themeColorProperties[getterString] = setterString; + }]; +} + +- (void)qmui_unregisterThemeColorProperties:(NSArray *)getters { + if (!self.qmuiTheme_themeColorProperties) return; + + [getters enumerateObjectsUsingBlock:^(NSString * _Nonnull getterString, NSUInteger idx, BOOL * _Nonnull stop) { + [self.qmuiTheme_themeColorProperties removeObjectForKey:getterString]; + }]; +} + +- (void)qmui_themeDidChangeByManager:(QMUIThemeManager *)manager identifier:(__kindof NSObject *)identifier theme:(__kindof NSObject *)theme { + if (![self _qmui_visible]) return; + + // 常见的 view 在 QMUIThemePrivate 里注册了 getter,在这里被调用 + [self.qmuiTheme_themeColorProperties enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull getterString, NSString * _Nonnull setterString, BOOL * _Nonnull stop) { + + SEL getter = NSSelectorFromString(getterString); + SEL setter = NSSelectorFromString(setterString); + + // 由于 tintColor 属性自带向下传递的性质,并且当值为 nil 时会自动从 superview 读取值,所以不需要在这里遍历修改,否则取出 tintColor 后再设置回去,会打破这个传递链 + if (getter == @selector(tintColor)) { + if (!self.qmui_tintColorCustomized) return; + } + + // 如果某个 UITabBarItem 处于选中状态,此时发生了主题变化,执行了 UITabBarSwappableImageView.image = image 的动作,就会把 selectedImage 设置为 normal image,无法恢复。所以对 UITabBarSwappableImageView 屏蔽掉 setImage 的刷新操作 + // https://github.com/Tencent/QMUI_iOS/issues/1122 + if ([self isKindOfClass:NSClassFromString(@"UITabBarSwappableImageView")] && getter == @selector(image)) { + return; + } + + // 注意,需要遍历的属性不一定都是 UIColor 类型,也有可能是 NSAttributedString,例如 UITextField.attributedText + BeginIgnorePerformSelectorLeaksWarning + id value = [self performSelector:getter]; + if (!value) return; + BOOL isValidatedColor = [value isKindOfClass:QMUIThemeColor.class] && (!manager || [((QMUIThemeColor *)value).managerName isEqual:manager.name]); + BOOL isValidatedImage = [value isKindOfClass:QMUIThemeImage.class] && (!manager || [((QMUIThemeImage *)value).managerName isEqual:manager.name]); + BOOL isValidatedEffect = [value isKindOfClass:QMUIThemeVisualEffect.class] && (!manager || [((QMUIThemeVisualEffect *)value).managerName isEqual:manager.name]); + BOOL isOtherObject = ![value isKindOfClass:UIColor.class] && ![value isKindOfClass:UIImage.class] && ![value isKindOfClass:UIVisualEffect.class];// 支持所有非 color、image、effect 的其他对象,例如 NSAttributedString + if (isOtherObject || isValidatedColor || isValidatedImage || isValidatedEffect) { + [self performSelector:setter withObject:value]; + } + EndIgnorePerformSelectorLeaksWarning + }]; + + // 特殊的 view 特殊处理 + // iOS 10-11 里当 UILabel.attributedText 的文字颜色都相同时,也无法使用 setNeedsDisplay 刷新样式,但只要某个 range 颜色不同就没问题,iOS 9、12-13 也没问题,这个通过 UILabel (QMUIThemeCompatibility) 兼容。 + if ([self isKindOfClass:UILabel.class]) { + [self setNeedsDisplay]; + } + + if ([self isKindOfClass:UITextView.class]) { +#ifdef IOS16_SDK_ALLOWED + if (@available(iOS 16.0, *)) { + // 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 + } + + // 输入框、搜索框的键盘跟随主题变化 + if (QMUICMIActivated) { + static NSArray *inputClasses = nil; + if (!inputClasses) inputClasses = @[UITextField.class, UITextView.class, UISearchBar.class];// 这里的 Class 与 UITextInputTraits(QMUI) 对齐 + [inputClasses enumerateObjectsUsingBlock:^(Class _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if ([self isKindOfClass:obj]) { + NSObject *input = (NSObject *)self; + if ([input respondsToSelector:@selector(keyboardAppearance)]) { + if (input.keyboardAppearance != KeyboardAppearance && !input.qmui_hasCustomizedKeyboardAppearance) { + input.qmui_keyboardAppearance = KeyboardAppearance; + } + } + *stop = YES; + } + }]; + } + + /** 这里去掉动画有 2 个原因: + 1. iOS 13 进入后台时会对 currentTraitCollection.userInterfaceStyle 做一次取反进行截图,以便在后台切换 Drak/Light 后能够更新 app 多任务缩略图,QMUI 响应了这个操作去调整取反后的 layer 的颜色,而在对 layer 设置属性的时候,如果包含了动画会导致截图不到最终的状态,这样会导致在后台切换 Drak/Light 后多任务缩略图无法及时更新。 + 2. 对于 UIView 层,修改 backgroundColor 默认是没有动画的,而 CALayer 修改 backgroundColor 会有隐式动画,这里为了在响应主题变化时颜色同步更新,统一把 CALayer 的动画去掉 + */ + [CALayer qmui_performWithoutAnimation:^{ + [self.layer qmui_setNeedsUpdateDynamicStyle]; + }]; + + if (self.qmui_themeDidChangeBlock) { + self.qmui_themeDidChangeBlock(); + } +} + +@end + +@implementation UIView (QMUITheme_Private) + +QMUISynthesizeIdStrongProperty(qmuiTheme_themeColorProperties, setQmuiTheme_themeColorProperties) + +- (BOOL)_qmui_visible { + BOOL hidden = self.hidden; + if ([self respondsToSelector:@selector(prepareForReuse)]) { + hidden = NO;// UITableViewCell 在 prepareForReuse 前会被 setHidden:YES,然后再被 setHidden:NO,然而后者是无效的,执行完之后依然是 hidden 为 YES,导致认为非 visible 而无法触发 themeDidChange,所以这里对 UITableViewCell 做特殊处理 + } + return !hidden && self.alpha > 0.01 && self.window; +} + +- (void)_qmui_themeDidChangeByManager:(QMUIThemeManager *)manager identifier:(__kindof NSObject *)identifier theme:(__kindof NSObject *)theme shouldEnumeratorSubviews:(BOOL)shouldEnumeratorSubviews { + [self qmui_themeDidChangeByManager:manager identifier:identifier theme:theme]; + if (shouldEnumeratorSubviews) { + [self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) { + [subview _qmui_themeDidChangeByManager:manager identifier:identifier theme:theme shouldEnumeratorSubviews:YES]; + }]; + } +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIViewController+QMUITheme.h b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIViewController+QMUITheme.h new file mode 100644 index 00000000..77b0dac2 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIViewController+QMUITheme.h @@ -0,0 +1,33 @@ +/** + * 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. + */ +// +// UIViewController+QMUITheme.h +// QMUIKit +// +// Created by MoLice on 2019/6/26. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIThemeManager; + +@interface UIViewController (QMUITheme) + +/** + 当主题变化时这个方法会被调用,不管当前 vc 是否处于可视状态。 + @param manager 当前的主题管理对象 + @param identifier 当前主题的标志,可自行修改参数类型为目标类型 + @param theme 当前主题对象,可自行修改参数类型为目标类型 + @warning 这个方法会在任何可能的时机被调用,不应该认为它一定比 viewDidLoad、viewWillAppear:、viewDidAppear: 晚。 + */ +- (void)qmui_themeDidChangeByManager:(QMUIThemeManager *)manager identifier:(__kindof NSObject *)identifier theme:(__kindof NSObject *)theme NS_REQUIRES_SUPER; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIViewController+QMUITheme.m b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIViewController+QMUITheme.m new file mode 100644 index 00000000..a44c4a3f --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIViewController+QMUITheme.m @@ -0,0 +1,51 @@ +/** + * 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. + */ +// +// UIViewController+QMUITheme.m +// QMUIKit +// +// Created by MoLice on 2019/6/26. +// + +#import "UIViewController+QMUITheme.h" +#import "QMUIModalPresentationViewController.h" + +@implementation UIViewController (QMUITheme) + +- (void)qmui_themeDidChangeByManager:(QMUIThemeManager *)manager identifier:(__kindof NSObject *)identifier theme:(__kindof NSObject *)theme { + /** + https://github.com/Tencent/QMUI_iOS/issues/1451 + + 这里有个取舍——到底应该对所有的 childViewControllers 无脑触发回调,还是仅对当前可视的 childViewController 触发。 + + 如果触发所有的 childViewControllers,可能带来的问题是某些 child 只是被 add 到 parent 里,尚未被展示到屏幕上(例如 tabBarController 默认只展示了第一个 child,后面几个 child 在没被切换时,都处于“init 了但还没 load view”状态,此时如果触发他们的回调,他们在回调里进行一些 view 的操作,可能会提前触发 loadView,这不一定符合开发者的预期。换句话说,这个回调可能比 viewWillAppear:、viewDidAppear: 都要早,这不一定符合直觉。 + + 如果只触发可视的 childViewController 的回调,则在 theme 切换后,从可视的 child 回到前一个 child,前面这个 child 无法感知到在它的生命周期内曾经有 theme 被切换过。假如这个 child 在内部有一些“记录当前是哪个 theme”的行为,则这些行为也会出错,并且唯一代替这个回调的方式就只有自己监听 QMUIThemeDidChangeNotification,相对而言比较绕。 + + 综上,还是选择无脑触发所有 childViewControllers 回调的做法,至于“这个回调可能比 viewWillAppear:、viewDidAppear: 都要早”的问题,暂时交给开发者自己意识。 + */ + [self.childViewControllers enumerateObjectsUsingBlock:^(__kindof UIViewController * _Nonnull childViewController, NSUInteger idx, BOOL * _Nonnull stop) { + [childViewController qmui_themeDidChangeByManager:manager identifier:identifier theme:theme]; + }]; + if (self.presentedViewController && self.presentedViewController.presentingViewController == self) { + [self.presentedViewController qmui_themeDidChangeByManager:manager identifier:identifier theme:theme]; + } +} + +@end + +@implementation QMUIModalPresentationViewController (QMUITheme) + +- (void)qmui_themeDidChangeByManager:(QMUIThemeManager *)manager identifier:(__kindof NSObject *)identifier theme:(__kindof NSObject *)theme { + [super qmui_themeDidChangeByManager:manager identifier:identifier theme:theme]; + if (self.contentViewController) { + [self.contentViewController qmui_themeDidChangeByManager:manager identifier:identifier theme:theme]; + } +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIVisualEffect+QMUITheme.h b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIVisualEffect+QMUITheme.h new file mode 100644 index 00000000..4a052f3c --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIVisualEffect+QMUITheme.h @@ -0,0 +1,76 @@ +/** + * 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. + */ +// +// UIVisualEffect+QMUITheme.h +// QMUIKit +// +// Created by MoLice on 2019/7/20. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIThemeManager; + +@protocol QMUIDynamicEffectProtocol + +@required + +/// 获取当前 UIVisualEffect 的标记名称,仅对 QMUIThemeVisualEffect 有效,其他 class 返回 nil。 +@property(nonatomic, copy, readonly) NSString *qmui_name; + +/// 获取当前 UIVisualEffect 的实际 effect(返回的 effect 必定不是 dynamic image) +@property(nonatomic, strong, readonly) __kindof UIVisualEffect *qmui_rawEffect; + +/// 标志当前 UIVisualEffect 对象是否为动态 effect(由 [UIVisualEffect qmui_effectWithThemeProvider:] 创建的 effect +@property(nonatomic, assign, readonly) BOOL qmui_isDynamicEffect; + +@end + +@interface UIVisualEffect (QMUITheme) + +/** + 生成一个动态的 UIVisualEffect 对象,每次使用该对象时都会动态根据当前的 QMUIThemeManager 主题返回对应的 effect。 + @param provider 当 UIVisualEffect 被使用时,这个 provider 会被调用,返回对应当前主题的 effect 值。请不要在这个 block 里做耗时操作。 + @return 一个动态的 UIVisualEffect 对象,被使用时才会返回实际的 effect 效果 + */ ++ (UIVisualEffect *)qmui_effectWithThemeProvider:(UIVisualEffect *(^)(__kindof QMUIThemeManager *manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme))provider; + +/** + 生成一个动态的 UIVisualEffect 对象,并以 name 为其标记。每次使用该对象时都会动态根据当前的 QMUIThemeManager 主题返回对应的 effect。 + @param name 动态 UIVisualEffect 的名称,默认为 nil + @param provider 当 UIVisualEffect 被使用时,这个 provider 会被调用,返回对应当前主题的 effect 值。请不要在这个 block 里做耗时操作。 + @return 一个动态的 UIVisualEffect 对象,被使用时才会返回实际的 effect 效果 +*/ ++ (UIVisualEffect *)qmui_effectWithName:(NSString * _Nullable)name + themeProvider:(UIVisualEffect *(^)(__kindof QMUIThemeManager *manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme))provider; + + +/** + 生成一个动态的 UIVisualEffect 对象。每次使用该对象时都会动态根据当前的 QMUIThemeManager name 和主题返回对应的 effect。 + @param managerName themeManager 的 name,用于区分不同维度的主题管理器 + @param provider 当 UIVisualEffect 被使用时,这个 provider 会被调用,返回对应当前主题的 effect 值。请不要在这个 block 里做耗时操作。 + @return 一个动态的 UIVisualEffect 对象,被使用时才会返回实际的 effect 效果 +*/ ++ (UIVisualEffect *)qmui_effectWithThemeManagerName:(__kindof NSObject *)managerName + provider:(UIVisualEffect *(^)(__kindof QMUIThemeManager *manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme))provider; + +/** + 生成一个动态的 UIVisualEffect 对象,并以 name 为其标记。每次使用该对象时都会动态根据当前的 QMUIThemeManager name 和主题返回对应的 effect。 + @param name 动态 UIVisualEffect 的名称,默认为 nil + @param managerName themeManager 的 name,用于区分不同维度的主题管理器 + @param provider 当 UIVisualEffect 被使用时,这个 provider 会被调用,返回对应当前主题的 effect 值。请不要在这个 block 里做耗时操作。 + @return 一个动态的 UIVisualEffect 对象,被使用时才会返回实际的 effect 效果 +*/ ++ (UIVisualEffect *)qmui_effectWithName:(NSString * _Nullable)name + themeManagerName:(__kindof NSObject *)managerName + provider:(UIVisualEffect *(^)(__kindof QMUIThemeManager *manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme))provider; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIVisualEffect+QMUITheme.m b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIVisualEffect+QMUITheme.m new file mode 100644 index 00000000..36b66da5 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITheme/UIVisualEffect+QMUITheme.m @@ -0,0 +1,140 @@ +/** + * 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. + */ +// +// UIVisualEffect+QMUITheme.m +// QMUIKit +// +// Created by MoLice on 2019/7/20. +// + +#import "UIVisualEffect+QMUITheme.h" +#import "QMUIThemeManager.h" +#import "QMUIThemeManagerCenter.h" +#import "QMUIThemePrivate.h" +#import "NSMethodSignature+QMUI.h" +#import "QMUICore.h" + +@implementation QMUIThemeVisualEffect + +- (id)copyWithZone:(NSZone *)zone { + QMUIThemeVisualEffect *effect = [[self class] allocWithZone:zone]; + effect.name = self.name; + effect.managerName = self.managerName; + effect.themeProvider = self.themeProvider; + return effect; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@,%@qmui_rawEffect = %@", [super description], self.name.length ? [NSString stringWithFormat:@" name = %@, ", self.name] : @" ", self.qmui_rawEffect]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { + NSMethodSignature *result = [super methodSignatureForSelector:aSelector]; + if (result) { + return result; + } + + result = [self.qmui_rawEffect methodSignatureForSelector:aSelector]; + if (result && [self.qmui_rawEffect respondsToSelector:aSelector]) { + return result; + } + + return [NSMethodSignature qmui_avoidExceptionSignature]; +} + +- (void)forwardInvocation:(NSInvocation *)anInvocation { + SEL selector = anInvocation.selector; + if ([self.qmui_rawEffect respondsToSelector:selector]) { + [anInvocation invokeWithTarget:self.qmui_rawEffect]; + } +} + +- (BOOL)respondsToSelector:(SEL)aSelector { + if ([super respondsToSelector:aSelector]) { + return YES; + } + + return [self.qmui_rawEffect respondsToSelector:aSelector]; +} + +- (BOOL)isKindOfClass:(Class)aClass { + if (aClass == QMUIThemeVisualEffect.class) return YES; + return [self.qmui_rawEffect isKindOfClass:aClass]; +} + +- (BOOL)isMemberOfClass:(Class)aClass { + if (aClass == QMUIThemeVisualEffect.class) return YES; + return [self.qmui_rawEffect isMemberOfClass:aClass]; +} + +- (BOOL)conformsToProtocol:(Protocol *)aProtocol { + return [self.qmui_rawEffect conformsToProtocol:aProtocol]; +} + +- (NSUInteger)hash { + return (NSUInteger)self.themeProvider; +} + +- (BOOL)isEqual:(id)object { + return NO; +} + +#pragma mark - + +- (NSString *)qmui_name { + return self.name; +} + +- (UIVisualEffect *)qmui_rawEffect { + QMUIThemeManager *manager = [QMUIThemeManagerCenter themeManagerWithName:self.managerName]; + return self.themeProvider(manager, manager.currentThemeIdentifier, manager.currentTheme).qmui_rawEffect; +} + +- (BOOL)qmui_isDynamicEffect { + return YES; +} + +@end + +@implementation UIVisualEffect (QMUITheme) + ++ (UIVisualEffect *)qmui_effectWithThemeProvider:(UIVisualEffect * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject * _Nullable, __kindof NSObject * _Nullable))provider { + return [self qmui_effectWithName:nil themeManagerName:QMUIThemeManagerNameDefault provider:provider]; +} + ++ (UIVisualEffect *)qmui_effectWithName:(NSString *)name themeProvider:(UIVisualEffect * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject * _Nullable, __kindof NSObject * _Nullable))provider { + return [self qmui_effectWithName:name themeManagerName:QMUIThemeManagerNameDefault provider:provider]; +} + ++ (UIVisualEffect *)qmui_effectWithThemeManagerName:(__kindof NSObject *)managerName provider:(UIVisualEffect * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject * _Nullable, __kindof NSObject * _Nullable))provider { + return [self qmui_effectWithName:nil themeManagerName:managerName provider:provider]; +} + ++ (UIVisualEffect *)qmui_effectWithName:(NSString *)name themeManagerName:(__kindof NSObject *)managerName provider:(UIVisualEffect * _Nonnull (^)(__kindof QMUIThemeManager * _Nonnull, __kindof NSObject * _Nullable, __kindof NSObject * _Nullable))provider { + QMUIThemeVisualEffect *effect = [[QMUIThemeVisualEffect alloc] init]; + effect.name = name; + effect.managerName = managerName; + effect.themeProvider = provider; + return (UIVisualEffect *)effect; +} + +#pragma mark - + +- (NSString *)qmui_name { + return nil; +} + +- (UIVisualEffect *)qmui_rawEffect { + return self; +} + +- (BOOL)qmui_isDynamicEffect { + return NO; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITips.h b/QMUI/QMUIKit/QMUIComponents/QMUITips.h index 3c27c314..3aeb0b4e 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITips.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUITips.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITips.h @@ -20,7 +20,7 @@ extern const NSInteger QMUITipsAutomaticallyHideToastSeconds; /// 默认的 parentView -#define DefaultTipsParentView [[[UIApplication sharedApplication] delegate] window] +#define DefaultTipsParentView (UIApplication.sharedApplication.delegate.window) /** * 简单封装了 QMUIToastView,支持弹出纯文本、loading、succeed、error、info 等五种 tips。如果这些接口还满足不了业务的需求,可以通过 QMUITips 的分类自行添加接口。 diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITips.m b/QMUI/QMUIKit/QMUIComponents/QMUITips.m index ec1e05bf..45aa0099 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUITips.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUITips.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITips.m @@ -139,6 +139,24 @@ - (void)showTipWithText:(NSString *)text detailText:(NSString *)detailText hideA } else if (delay > 0) { [self hideAnimated:YES afterDelay:delay]; } + + [self postAccessibilityAnnouncement:text detailText:detailText]; +} + +- (void)postAccessibilityAnnouncement:(NSString *)text detailText:(NSString *)detailText { + NSString *announcementString = nil; + if (text) { + announcementString = text; + } + if (detailText) { + announcementString = announcementString ? [text stringByAppendingFormat:@", %@", detailText] : detailText; + } + if (announcementString) { + // 发送一个让VoiceOver播报的Announcement,帮助视障用户获取toast内的信息,但是这个播报会被即时打断而不生效,所以在这里延时1秒发送此通知。 + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcementString); + }); + } } + (NSTimeInterval)smartDelaySecondsForTipsText:(NSString *)text { diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIVisualEffectView.m b/QMUI/QMUIKit/QMUIComponents/QMUIVisualEffectView.m deleted file mode 100644 index 14c1727c..00000000 --- a/QMUI/QMUIKit/QMUIComponents/QMUIVisualEffectView.m +++ /dev/null @@ -1,58 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUIVisualEffectView.m -// QMUIKit -// -// Created by QMUI Team on 2018/6/19. -// - -#import "QMUIVisualEffectView.h" -#import "QMUICore.h" -#import "CALayer+QMUI.h" - -@interface QMUIVisualEffectView () - -@property(nonatomic, strong) CALayer *foregroundLayer; - -@end - -@implementation QMUIVisualEffectView - -- (instancetype)initWithEffect:(nullable UIVisualEffect *)effect { - if (self = [super initWithEffect:effect]) { - [self didInitialize]; - } - return self; -} - -- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialize]; - } - return self; -} - -- (void)didInitialize { - self.foregroundLayer = [CALayer layer]; - [self.foregroundLayer qmui_removeDefaultAnimations]; - [self.contentView.layer addSublayer:self.foregroundLayer]; -} - -- (void)setForegroundColor:(UIColor *)foregroundColor { - _foregroundColor = foregroundColor; - self.foregroundLayer.backgroundColor = foregroundColor.CGColor; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - self.foregroundLayer.frame = self.contentView.bounds; -} - -@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIWeakObjectContainer.h b/QMUI/QMUIKit/QMUIComponents/QMUIWeakObjectContainer.h index 75f63960..c478400c 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIWeakObjectContainer.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIWeakObjectContainer.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIWeakObjectContainer.h @@ -15,16 +15,23 @@ #import +NS_ASSUME_NONNULL_BEGIN + /** 一个常见的场景:当通过 objc_setAssociatedObject 以弱引用的方式(OBJC_ASSOCIATION_ASSIGN)绑定对象A时,假如对象A稍后被释放了,则通过 objc_getAssociatedObject 再次试图访问对象A时会导致野指针。 这时你可以将对象A包装为一个 QMUIWeakObjectContainer,并改为通过强引用方式(OBJC_ASSOCIATION_RETAIN_NONATOMIC/OBJC_ASSOCIATION_RETAIN)绑定这个 QMUIWeakObjectContainer,进而安全地获取原始对象A。 */ -@interface QMUIWeakObjectContainer : NSObject +@interface QMUIWeakObjectContainer : NSProxy /// 将一个 object 包装到一个 QMUIWeakObjectContainer 里 - (instancetype)initWithObject:(id)object; +- (instancetype)init; ++ (instancetype)containerWithObject:(id)object; /// 获取原始对象 object,如果 object 已被释放则该属性返回 nil -@property (nonatomic, weak) id object; +@property (nullable, nonatomic, weak) id object; +@property(nonatomic, assign, readonly) BOOL isQMUIWeakObjectContainer; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIWeakObjectContainer.m b/QMUI/QMUIKit/QMUIComponents/QMUIWeakObjectContainer.m index f1ccfba2..001a313e 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIWeakObjectContainer.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIWeakObjectContainer.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIWeakObjectContainer.m @@ -15,13 +15,85 @@ #import "QMUIWeakObjectContainer.h" +// from https://github.com/ibireme/YYKit/blob/master/YYKit/Utility/YYWeakProxy.m + @implementation QMUIWeakObjectContainer - (instancetype)initWithObject:(id)object { - if (self = [super init]) { - _object = object; - } + _object = object; + return self; +} + +- (instancetype)init { return self; } ++ (instancetype)containerWithObject:(id)object { + return [[self alloc] initWithObject:object]; +} + +- (BOOL)isQMUIWeakObjectContainer { + return YES; +} + +- (id)forwardingTargetForSelector:(SEL)selector { + return _object; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { + void *null = NULL; + [invocation setReturnValue:&null]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { + return [NSObject instanceMethodSignatureForSelector:@selector(init)]; +} + +- (BOOL)respondsToSelector:(SEL)aSelector { + if (aSelector == @selector(isQMUIWeakObjectContainer)) { + return YES; + } + return [_object respondsToSelector:aSelector]; +} + +- (BOOL)isEqual:(id)object { + return [_object isEqual:object]; +} + +- (NSUInteger)hash { + return [_object hash]; +} + +- (Class)superclass { + return [_object superclass]; +} + +- (Class)class { + return [_object class]; +} + +- (BOOL)isKindOfClass:(Class)aClass { + return [_object isKindOfClass:aClass]; +} + +- (BOOL)isMemberOfClass:(Class)aClass { + return [_object isMemberOfClass:aClass]; +} + +- (BOOL)conformsToProtocol:(Protocol *)aProtocol { + return [_object conformsToProtocol:aProtocol]; +} + +- (BOOL)isProxy { + return YES; +} + +- (NSString *)description { + return [_object description]; +} + +- (NSString *)debugDescription { + return [_object debugDescription]; +} + @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.h b/QMUI/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.h new file mode 100644 index 00000000..edbf19d1 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.h @@ -0,0 +1,59 @@ +/** + * 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. + */ +// +// QMUIWindowSizeMonitor.h +// qmuidemo +// +// Created by ziezheng on 2019/5/27. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol QMUIWindowSizeMonitorProtocol + +@optional + +/** + 当继承自 UIResponder 的对象,比如 UIView 或 UIViewController 实现了这个方法时,其所属的 window 在大小发生改变后在这个方法回调。 + @note 类似系统的 [-viewWillTransitionToSize:withTransitionCoordinator:],但是系统这个方法回调时 window 的大小实际上还未发生改变,如果你需要在 window 大小发生之后且在 layout 之前来处理一些逻辑时,可以放到这个方法去实现。 + @note 如果子类和父类同时实现了该方法,则两个方法均会被调用,调用顺序是先父类后子类。 + @param size 所属窗口的新大小 + */ + +- (void)windowDidTransitionToSize:(CGSize)size; + +@end + +@interface UIResponder (QMUIWindowSizeMonitor) + +@end + +typedef void (^QMUIWindowSizeObserverHandler)(CGSize newWindowSize); + +@interface NSObject (QMUIWindowSizeMonitor) + +/** + 为当前对象添加主窗口 (UIApplication Delegate Window)的大小变化的监听,同一对象可重复添加多个监听,当对象销毁时监听自动失效。 + + @param handler 窗口大小发生改变时的回调 + */ +- (void)qmui_addSizeObserverForMainWindow:(QMUIWindowSizeObserverHandler)handler; +/** + 为当前对象添加指定窗口的大小变化监听,同一对象可重复添加多个监听,当对象销毁时监听自动失效。 + + @param window 要监听的窗口 + @param handler 窗口大小发生改变时的回调 + */ +- (void)qmui_addSizeObserverForWindow:(UIWindow *)window handler:(QMUIWindowSizeObserverHandler)handler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m b/QMUI/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m new file mode 100644 index 00000000..a926adad --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m @@ -0,0 +1,213 @@ +/** + * 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. + */ +// +// QMUIWindowSizeMonitor.m +// qmuidemo +// +// Created by ziezheng on 2019/5/27. +// + +#import "QMUIWindowSizeMonitor.h" +#import "QMUICore.h" +#import "NSPointerArray+QMUI.h" + +@interface NSObject (QMUIWindowSizeMonitor_Private) + +@property(nonatomic, readonly) NSMutableDictionary *qwsm_windowSizeChangeHandlers; + +@end + +@interface UIResponder (QMUIWindowSizeMonitor_Private) + +@property(nonatomic, weak) UIWindow *qwsm_previousWindow; + +@end + + +@interface UIWindow (QMUIWindowSizeMonitor_Private) + +@property(nonatomic, assign) CGSize qwsm_previousSize; +@property(nonatomic, readonly) NSPointerArray *qwsm_sizeObservers; +@property(nonatomic, readonly) NSPointerArray *qwsm_canReceiveWindowDidTransitionToSizeResponders; + +- (void)qwsm_addSizeObserver:(NSObject *)observer; + +@end + + + +@implementation NSObject (QMUIWindowSizeMonitor) + +- (void)qmui_addSizeObserverForMainWindow:(QMUIWindowSizeObserverHandler)handler { + [self qmui_addSizeObserverForWindow:UIApplication.sharedApplication.delegate.window handler:handler]; +} + +- (void)qmui_addSizeObserverForWindow:(UIWindow *)window handler:(QMUIWindowSizeObserverHandler)handler { + QMUIAssert(window != nil, @"NSObject (QMUIWindowSizeMonitor)", @"%s, window should not be nil.", __func__); + + struct Block_literal { + void *isa; + int flags; + int reserved; + void (*__FuncPtr)(void *, ...); + }; + + void * blockFuncPtr = ((__bridge struct Block_literal *)handler)->__FuncPtr; + for (QMUIWindowSizeObserverHandler handler in self.qwsm_windowSizeChangeHandlers.allKeys) { + // 由于利用 block 的 __FuncPtr 指针来判断同一个实现的 block 过滤掉,防止重复添加监听 + if (((__bridge struct Block_literal *)handler)->__FuncPtr == blockFuncPtr) { + return; + } + } + + self.qwsm_windowSizeChangeHandlers[(id)handler] = [[QMUIWeakObjectContainer alloc] initWithObject:window]; + [window qwsm_addSizeObserver:self]; +} + +- (NSMutableDictionary *)qwsm_windowSizeChangeHandlers { + NSMutableDictionary *_handlers = objc_getAssociatedObject(self, _cmd); + if (!_handlers) { + _handlers = [[NSMutableDictionary alloc] init]; + objc_setAssociatedObject(self, _cmd, _handlers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return _handlers; +} + +@end + +@implementation UIWindow (QMUIWindowSizeMonitor) + +QMUISynthesizeCGSizeProperty(qwsm_previousSize, setQwsm_previousSize) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + void (^notifyNewSizeBlock)(UIWindow *, CGRect) = ^(UIWindow *selfObject, CGRect firstArgv) { + CGSize newSize = selfObject.bounds.size; + if (!CGSizeEqualToSize(newSize, selfObject.qwsm_previousSize)) { + if (!CGSizeEqualToSize(selfObject.qwsm_previousSize, CGSizeZero)) { + [selfObject qwsm_notifyWithNewSize:newSize]; + } + selfObject.qwsm_previousSize = selfObject.bounds.size; + } + }; + + ExtendImplementationOfVoidMethodWithSingleArgument([UIWindow class], @selector(setFrame:), CGRect, notifyNewSizeBlock); + + ExtendImplementationOfVoidMethodWithSingleArgument([UIWindow class], @selector(setBounds:), CGRect, notifyNewSizeBlock); + + OverrideImplementation([UIView class], @selector(willMoveToWindow:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^void(UIView *selfObject, UIWindow *newWindow) { + + void (*originSelectorIMP)(id, SEL, UIWindow *); + originSelectorIMP = (void (*)(id, SEL, UIWindow *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, newWindow); + + if (newWindow) { + if ([selfObject respondsToSelector:@selector(windowDidTransitionToSize:)]) { + [newWindow qwsm_addDidTransitionToSizeMethodReceiver:selfObject]; + } + UIResponder *nextResponder = [selfObject nextResponder]; + if ([nextResponder isKindOfClass:[UIViewController class]] && [nextResponder respondsToSelector:@selector(windowDidTransitionToSize:)]) { + [newWindow qwsm_addDidTransitionToSizeMethodReceiver:nextResponder]; + } + } + + }; + }); + }); +} + + +- (void)qwsm_addSizeObserver:(NSObject *)observer { + if ([self.qwsm_sizeObservers qmui_containsPointer:(__bridge void *)(observer)]) return; + [self.qwsm_sizeObservers addPointer:(__bridge void *)(observer)]; +} + +- (void)qwsm_removeSizeObserver:(NSObject *)observer { + NSUInteger index = [self.qwsm_sizeObservers qmui_indexOfPointer:(__bridge void *)observer]; + if (index != NSNotFound) { + [self.qwsm_sizeObservers removePointerAtIndex:index]; + } +} + +- (void)qwsm_addDidTransitionToSizeMethodReceiver:(UIResponder *)receiver { + if ([self.qwsm_canReceiveWindowDidTransitionToSizeResponders qmui_containsPointer:(__bridge void *)(receiver)]) return; + if (receiver.qwsm_previousWindow && receiver.qwsm_previousWindow != self) { + [receiver.qwsm_previousWindow qwsm_removeDidTransitionToSizeMethodReceiver:receiver]; + } + receiver.qwsm_previousWindow = self; + [self.qwsm_canReceiveWindowDidTransitionToSizeResponders addPointer:(__bridge void *)(receiver)]; +} + +- (void)qwsm_removeDidTransitionToSizeMethodReceiver:(UIResponder *)receiver { + NSUInteger index = [self.qwsm_canReceiveWindowDidTransitionToSizeResponders qmui_indexOfPointer:(__bridge void *)(receiver)]; + if (index != NSNotFound) { + [self.qwsm_canReceiveWindowDidTransitionToSizeResponders removePointerAtIndex:index]; + } +} + + +- (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]; + [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++) { + UIResponder *responder = [self.qwsm_canReceiveWindowDidTransitionToSizeResponders pointerAtIndex:i]; + // call superclass automatically + Method lastMethod = NULL; + NSMutableArray *selectorIMPArray = [NSMutableArray array]; + for (Class responderClass = object_getClass(responder); responderClass != [UIResponder class]; responderClass = class_getSuperclass(responderClass)) { + Method methodOfClass = class_getInstanceMethod(responderClass, @selector(windowDidTransitionToSize:)); + if (methodOfClass == NULL) break; + if (methodOfClass == lastMethod) continue; + void (*selectorIMP)(id, SEL, CGSize) = (void (*)(id, SEL, CGSize))method_getImplementation(methodOfClass); + [selectorIMPArray addObject:[NSValue valueWithPointer:selectorIMP]]; + lastMethod = methodOfClass; + } + // call the superclass before calling the subclass + for (NSInteger i = selectorIMPArray.count - 1; i >= 0; i--) { + void (*selectorIMP)(id, SEL, CGSize) = selectorIMPArray[i].pointerValue; + selectorIMP(responder, @selector(windowDidTransitionToSize:), newSize); + } + } +} + +- (NSPointerArray *)qwsm_sizeObservers { + NSPointerArray *qwsm_sizeObservers = objc_getAssociatedObject(self, _cmd); + if (!qwsm_sizeObservers) { + qwsm_sizeObservers = [NSPointerArray weakObjectsPointerArray]; + objc_setAssociatedObject(self, _cmd, qwsm_sizeObservers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return qwsm_sizeObservers; +} + +- (NSPointerArray *)qwsm_canReceiveWindowDidTransitionToSizeResponders { + NSPointerArray *qwsm_responders = objc_getAssociatedObject(self, _cmd); + if (!qwsm_responders) { + qwsm_responders = [NSPointerArray weakObjectsPointerArray]; + objc_setAssociatedObject(self, _cmd, qwsm_responders, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return qwsm_responders; +} + +@end + +@implementation UIResponder (QMUIWindowSizeMonitor) + +QMUISynthesizeIdWeakProperty(qwsm_previousWindow, setQwsm_previousWindow) + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIZoomImageView.h b/QMUI/QMUIKit/QMUIComponents/QMUIZoomImageView.h index 1a2a2671..4265a1d0 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIZoomImageView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIZoomImageView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIZoomImageView.h @@ -20,7 +20,6 @@ @class QMUIZoomImageView; @class QMUIEmptyView; @class QMUIButton; -@class QMUISlider; @class QMUIZoomImageViewVideoToolbar; @class QMUIPieProgressView; @@ -80,10 +79,10 @@ @property(nonatomic, strong, readonly) UIImageView *imageView; /// 设置当前要显示的 Live Photo,会把 image/video 相关内容清空,因此注意不要直接通过 livePhotoView.livePhoto 来设置 -@property(nonatomic, weak) PHLivePhoto *livePhoto NS_AVAILABLE_IOS(9_1); +@property(nonatomic, weak) PHLivePhoto *livePhoto; /// 用于显示 Live Photo 的 view,仅在 iOS 9.1 及以后才有效 -@property(nonatomic, strong, readonly) PHLivePhotoView *livePhotoView NS_AVAILABLE_IOS(9_1); +@property(nonatomic, strong, readonly) PHLivePhotoView *livePhotoView; /// 设置当前要显示的 video ,会把 image/livePhoto 相关内容清空,因此注意不要直接通过 videoPlayerLayer 来设置 @property(nonatomic, weak) AVPlayerItem *videoPlayerItem; @@ -95,7 +94,7 @@ // @see QMUIZoomImageViewVideoToolbar @property(nonatomic, strong, readonly) QMUIZoomImageViewVideoToolbar *videoToolbar; -// 视频底部控制条的 margins,会在此基础上自动叠加 QMUIZoomImageView.qmui_safeAreaInsets,因此无需考虑在 iPhone X 下的兼容 +// 视频底部控制条的 margins,会在此基础上自动叠加 QMUIZoomImageView.safeAreaInsets,因此无需考虑在 iPhone X 下的兼容 // 默认值为 {0, 25, 25, 18} @property(nonatomic, assign) UIEdgeInsets videoToolbarMargins UI_APPEARANCE_SELECTOR; @@ -163,7 +162,7 @@ @property(nonatomic, strong, readonly) QMUIButton *playButton; @property(nonatomic, strong, readonly) QMUIButton *pauseButton; -@property(nonatomic, strong, readonly) QMUISlider *slider; +@property(nonatomic, strong, readonly) UISlider *slider; @property(nonatomic, strong, readonly) UILabel *sliderLeftLabel; @property(nonatomic, strong, readonly) UILabel *sliderRightLabel; diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIZoomImageView.m b/QMUI/QMUIKit/QMUIComponents/QMUIZoomImageView.m index 4353e4b3..9059d605 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIZoomImageView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIZoomImageView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIZoomImageView.m @@ -21,11 +21,15 @@ #import "UIColor+QMUI.h" #import "UIScrollView+QMUI.h" #import "QMUIButton.h" -#import "QMUISlider.h" -#import -#import "UIControl+QMUI.h" +#import "UISlider+QMUI.h" #import "UILabel+QMUI.h" #import "QMUIPieProgressView.h" +#import +#import +#import +#import +#import "CALayer+QMUI.h" +#import "NSShadow+QMUI.h" #define kIconsColor UIColorMakeWithRGBA(255, 255, 255, .75) @@ -67,6 +71,7 @@ @implementation QMUIZoomImageView @synthesize cloudDownloadRetryButton = _cloudDownloadRetryButton; - (void)didMoveToWindow { + [super didMoveToWindow]; // 当 self.window 为 nil 时说明此 view 被移出了可视区域(比如所在的 controller 被 pop 了),此时应该停止视频播放 if (!self.window) { [self endPlayingVideo]; @@ -84,9 +89,7 @@ - (instancetype)initWithFrame:(CGRect)frame { self.scrollView.minimumZoomScale = 0; self.scrollView.maximumZoomScale = self.maximumZoomScale; self.scrollView.delegate = self; - if (@available(iOS 11, *)) { - self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } + self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; [self addSubview:self.scrollView]; _emptyView = [[QMUIEmptyView alloc] init]; @@ -135,7 +138,7 @@ - (void)layoutSubviews { if (_videoToolbar) { _videoToolbar.frame = ({ - UIEdgeInsets margins = UIEdgeInsetsConcat(self.videoToolbarMargins, self.qmui_safeAreaInsets); + UIEdgeInsets margins = UIEdgeInsetsConcat(self.videoToolbarMargins, self.safeAreaInsets); CGFloat width = CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(margins); CGFloat height = [_videoToolbar sizeThatFits:CGSizeMake(width, CGFLOAT_MAX)].height; CGRectFlatMake(margins.left, CGRectGetHeight(self.bounds) - margins.bottom - height, width, height); @@ -180,13 +183,15 @@ - (void)initImageViewIfNeeded { - (void)setImage:(UIImage *)image { _image = image; - // 释放以节省资源 - [_livePhotoView removeFromSuperview]; - _livePhotoView = nil; - [self destroyVideoRelatedObjectsIfNeeded]; + if (image) { + self.livePhoto = nil; + self.videoPlayerItem = nil; + } if (!image) { _imageView.image = nil; + [_imageView removeFromSuperview]; + _imageView = nil; return; } self.imageView.image = image; @@ -210,19 +215,20 @@ - (PHLivePhotoView *)livePhotoView { - (void)setLivePhoto:(PHLivePhoto *)livePhoto { _livePhoto = livePhoto; - [_imageView removeFromSuperview]; - _imageView = nil; - [self destroyVideoRelatedObjectsIfNeeded]; + if (livePhoto) { + self.image = nil; + self.videoPlayerItem = nil; + } if (!livePhoto) { _livePhotoView.livePhoto = nil; + [_livePhotoView removeFromSuperview]; + _livePhotoView = nil; return; } [self initLivePhotoViewIfNeeded]; - if (@available(iOS 9.1, *)) { - _livePhotoView.livePhoto = livePhoto; - } + _livePhotoView.livePhoto = livePhoto; _livePhotoView.hidden = NO; // 更新 livePhotoView 的大小时,livePhotoView 可能已经被缩放过,所以要应用当前的缩放 @@ -232,13 +238,11 @@ - (void)setLivePhoto:(PHLivePhoto *)livePhoto { } - (void)initLivePhotoViewIfNeeded { - if (@available(iOS 9.1, *)) { - if (_livePhotoView) { - return; - } - _livePhotoView = [[PHLivePhotoView alloc] init]; - [self.scrollView addSubview:_livePhotoView]; + if (_livePhotoView) { + return; } + _livePhotoView = [[PHLivePhotoView alloc] init]; + [self.scrollView addSubview:_livePhotoView]; } #pragma mark - Image Scale @@ -257,7 +261,9 @@ - (void)setMaximumZoomScale:(CGFloat)maximumZoomScale { } - (CGFloat)minimumZoomScale { - if (!self.image && !self.livePhoto && !self.videoPlayerItem) { + BOOL isLivePhoto = !!self.livePhoto; + + if (!self.image && !isLivePhoto && !self.videoPlayerItem) { return 1; } @@ -265,7 +271,7 @@ - (CGFloat)minimumZoomScale { CGSize mediaSize = CGSizeZero; if (self.image) { mediaSize = self.image.size; - } else if (self.livePhoto) { + } else if (isLivePhoto) { mediaSize = self.livePhoto.size; } else if (self.videoPlayerItem) { mediaSize = self.videoSize; @@ -390,9 +396,10 @@ - (void)handleDidEndZooming { - (BOOL)enabledZoomImageView { BOOL enabledZoom = YES; + BOOL isLivePhoto = !!self.livePhoto; if ([self.delegate respondsToSelector:@selector(enabledZoomViewInZoomImageView:)]) { enabledZoom = [self.delegate enabledZoomViewInZoomImageView:self]; - } else if (!self.image && !self.livePhoto && !self.videoPlayerItem) { + } else if (!self.image && !isLivePhoto && !self.videoPlayerItem) { enabledZoom = NO; } return enabledZoom; @@ -403,13 +410,19 @@ - (BOOL)enabledZoomImageView { - (void)setVideoPlayerItem:(AVPlayerItem *)videoPlayerItem { _videoPlayerItem = videoPlayerItem; - [_livePhotoView removeFromSuperview]; - _livePhotoView = nil; - [_imageView removeFromSuperview]; - _imageView = nil; + if (videoPlayerItem) { + self.livePhoto = nil; + self.image = nil; + [self hideViews]; + } + + // 移除旧的 videoPlayer 时,同时移除相应的 timeObserver + if (self.videoPlayer) { + [self removePlayerTimeObserver]; + } if (!videoPlayerItem) { - [self hideViews]; + [self destroyVideoRelatedObjectsIfNeeded]; return; } @@ -424,11 +437,6 @@ - (void)setVideoPlayerItem:(AVPlayerItem *)videoPlayerItem { } } - if (self.videoPlayer) { - // 移除旧的 videoPlayer 时,同时移除相应的 timeObserver - [self removePlayerTimeObserver]; - } - self.videoPlayer = [AVPlayer playerWithPlayerItem:videoPlayerItem]; [self initVideoRelatedViewsIfNeeded]; _videoPlayerLayer.player = self.videoPlayer; @@ -440,7 +448,6 @@ - (void)setVideoPlayerItem:(AVPlayerItem *)videoPlayerItem { [self configVideoProgressSlider]; - [self hideViews]; self.videoPlayerLayer.hidden = NO; self.videoCenteredPlayButton.hidden = NO; self.videoToolbar.playButton.hidden = NO; @@ -652,6 +659,7 @@ - (void)destroyVideoRelatedObjectsIfNeeded { _videoCenteredPlayButton = nil; self.videoPlayer = nil; + _videoPlayerLayer.player = nil; } - (void)setVideoToolbarMargins:(UIEdgeInsets)videoToolbarMargins { @@ -852,6 +860,8 @@ - (void)showEmptyViewWithText:(NSString *)text [self.emptyView setActionButtonTitle:buttonTitle]; [self.emptyView.actionButton removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents]; [self.emptyView.actionButton addTarget:buttonTarget action:action forControlEvents:UIControlEventTouchUpInside]; + self.emptyView.hidden = NO; + [self setNeedsLayout]; } - (void)hideEmptyView { @@ -1021,11 +1031,11 @@ - (instancetype)initWithFrame:(CGRect)frame { [self.pauseButton setImage:self.pauseButtonImage forState:UIControlStateNormal]; [self addSubview:self.pauseButton]; - _slider = [[QMUISlider alloc] init]; + _slider = [[UISlider alloc] init]; self.slider.minimumTrackTintColor = UIColorMake(195, 195, 195); self.slider.maximumTrackTintColor = UIColorMake(95, 95, 95); - self.slider.thumbSize = CGSizeMake(12, 12); - self.slider.thumbColor = UIColorWhite; + self.slider.qmui_thumbSize = CGSizeMake(12, 12); + self.slider.qmui_thumbColor = UIColorWhite; [self addSubview:self.slider]; _sliderLeftLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(12) textColor:UIColorWhite]; @@ -1036,10 +1046,7 @@ - (instancetype)initWithFrame:(CGRect)frame { [self.sliderRightLabel qmui_setTheSameAppearanceAsLabel:self.sliderLeftLabel]; [self addSubview:self.sliderRightLabel]; - self.layer.shadowColor = UIColorBlack.CGColor; - self.layer.shadowOpacity = .5; - self.layer.shadowOffset = CGSizeMake(0, 0); - self.layer.shadowRadius = 10; + self.layer.qmui_shadow = [NSShadow qmui_shadowWithColor:[UIColorBlack colorWithAlphaComponent:.5] shadowOffset:CGSizeZero shadowRadius:10]; } return self; } diff --git a/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.h b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.h index e6406c0d..a5d8c834 100644 --- a/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.h +++ b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIStaticTableViewCellData.h @@ -16,6 +16,8 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + @class QMUITableViewCell; typedef NS_ENUM(NSInteger, QMUIStaticTableViewCellAccessoryType) { @@ -37,7 +39,7 @@ typedef NS_ENUM(NSInteger, QMUIStaticTableViewCellAccessoryType) { @property(nonatomic, assign) NSInteger identifier; /// 当前 cellData 所对应的 indexPath -@property(nonatomic, strong, readonly) NSIndexPath *indexPath; +@property(nonatomic, strong, readonly, nullable) NSIndexPath *indexPath; /// cell 要使用的 class,默认为 QMUITableViewCell,若要改为自定义 class,必须是 QMUITableViewCell 的子类 @property(nonatomic, assign) Class cellClass; @@ -49,60 +51,70 @@ typedef NS_ENUM(NSInteger, QMUIStaticTableViewCellAccessoryType) { @property(nonatomic, assign) CGFloat height; /// cell 左边要显示的图片,将会被设置到 cell.imageView.image -@property(nonatomic, strong) UIImage *image; +@property(nonatomic, strong, nullable) UIImage *image; /// cell 的文字,将会被设置到 cell.textLabel.text -@property(nonatomic, copy) NSString *text; +@property(nonatomic, copy, nullable) NSString *text; /// cell 的详细文字,将会被设置到 cell.detailTextLabel.text,所以要求 cellData.style 的值必须是带 detailTextLabel 类型的 style -@property(nonatomic, copy) NSString *detailText; +@property(nonatomic, copy, nullable) NSString *detailText; /// 会自动在 tableView:cellForRowAtIndexPath: 里调用,这样就不需要实现 cellForRow -@property(nonatomic, copy) void (^cellForRowBlock)(UITableView *tableView, __kindof QMUITableViewCell *cell, QMUIStaticTableViewCellData *cellData); +@property(nonatomic, copy, nullable) void (^cellForRowBlock)(UITableView *tableView, __kindof QMUITableViewCell *cell, QMUIStaticTableViewCellData *cellData); + +/// 会自动在 tableView:didSelectRowAtIndexPath: 里调用,当实现了这个属性时,didSelectTarget/didSelectAction 会失效 +@property(nonatomic, copy, nullable) void (^didSelectBlock)(UITableView *tableView, QMUIStaticTableViewCellData *cellData); -/// 当 cell 的点击事件被触发时,要由哪个对象来接收 -@property(nonatomic, assign) id didSelectTarget; +/// 当 cell 的点击事件被触发时,要由哪个对象来接收,当实现了 didSelectBlock 时本属性无效 +@property(nonatomic, assign, nullable) id didSelectTarget; -/// 当 cell 的点击事件被触发时,要向 didSelectTarget 指针发送什么消息以响应事件 +/// 当 cell 的点击事件被触发时,要向 didSelectTarget 指针发送什么消息以响应事件,当实现了 didSelectBlock 时本属性无效 /// @warning 这个 selector 接收一个参数,这个参数也即当前的 QMUIStaticTableViewCellData 对象 -@property(nonatomic, assign) SEL didSelectAction; +@property(nonatomic, assign, nullable) SEL didSelectAction; /// cell 右边的 accessoryView 的类型 @property(nonatomic, assign) QMUIStaticTableViewCellAccessoryType accessoryType; /// 配合 accessoryType 使用,不同的 accessoryType 需要配合不同 class 的 accessoryValueObject 使用。例如 QMUIStaticTableViewCellAccessoryTypeSwitch 要求传 @YES 或 @NO 用于控制 UISwitch.on 属性。 /// @warning 目前也仅支持与 QMUIStaticTableViewCellAccessoryTypeSwitch 搭配使用。 -@property(nonatomic, strong) NSObject *accessoryValueObject; +@property(nonatomic, strong, nullable) NSObject *accessoryValueObject; -/// 当 accessoryType 是某些带 UIControl 的控件时,可通过这两个属性来为 accessoryView 添加操作事件。 -/// 目前支持的类型包括:QMUIStaticTableViewCellAccessoryTypeDetailDisclosureButton、QMUIStaticTableViewCellAccessoryTypeDetailButton、QMUIStaticTableViewCellAccessoryTypeSwitch +/// 当 accessoryType 是 QMUIStaticTableViewCellAccessoryTypeDetailDisclosureButton、QMUIStaticTableViewCellAccessoryTypeDetailButton 时,点击按钮会触发这个 block,当实现了这个属性时,accessoryTarget/accessoryAction 会失效。 +@property(nonatomic, copy, nullable) void (^accessoryBlock)(UITableView *tableView, QMUIStaticTableViewCellData *cellData); + +/// 当 accessoryType 是 QMUIStaticTableViewCellAccessoryTypeSwitch 时,切换 UISwitch 开关会触发这个 block,当实现了这个属性时,accessoryTarget/accessoryAction 会失效。 +@property(nonatomic, copy, nullable) void (^accessorySwitchBlock)(UITableView *tableView, QMUIStaticTableViewCellData *cellData, UISwitch *switcher); + +/// 当 accessoryType 是 QMUIStaticTableViewCellAccessoryTypeDetailDisclosureButton、QMUIStaticTableViewCellAccessoryTypeDetailButton、QMUIStaticTableViewCellAccessoryTypeSwitch 时,可通过这两个属性来为 accessoryView 添加操作事件。 /// @warning 这个 selector 接收一个参数,与 didSelectAction 一样,这个参数一般情况下也是当前的 QMUIStaticTableViewCellData 对象,仅在 Switch 时会传 UISwitch 控件的实例 -@property(nonatomic, assign) id accessoryTarget; -@property(nonatomic, assign) SEL accessoryAction; +@property(nonatomic, assign, nullable) id accessoryTarget; +@property(nonatomic, assign, nullable) SEL accessoryAction; + (instancetype)staticTableViewCellDataWithIdentifier:(NSInteger)identifier - image:(UIImage *)image - text:(NSString *)text - detailText:(NSString *)detailText - didSelectTarget:(id)didSelectTarget - didSelectAction:(SEL)didSelectAction + image:(nullable UIImage *)image + text:(nullable NSString *)text + detailText:(nullable NSString *)detailText + didSelectTarget:(nullable id)didSelectTarget + didSelectAction:(nullable SEL)didSelectAction accessoryType:(QMUIStaticTableViewCellAccessoryType)accessoryType; + (instancetype)staticTableViewCellDataWithIdentifier:(NSInteger)identifier cellClass:(Class)cellClass style:(UITableViewCellStyle)style height:(CGFloat)height - image:(UIImage *)image - text:(NSString *)text - detailText:(NSString *)detailText - didSelectTarget:(id)didSelectTarget - didSelectAction:(SEL)didSelectAction + image:(nullable UIImage *)image + text:(nullable NSString *)text + detailText:(nullable NSString *)detailText + didSelectTarget:(nullable id)didSelectTarget + didSelectAction:(nullable SEL)didSelectAction accessoryType:(QMUIStaticTableViewCellAccessoryType)accessoryType - accessoryValueObject:(NSObject *)accessoryValueObject - accessoryTarget:(id)accessoryTarget - accessoryAction:(SEL)accessoryAction; + accessoryValueObject:(nullable NSObject *)accessoryValueObject + accessoryTarget:(nullable id)accessoryTarget + accessoryAction:(nullable SEL)accessoryAction; + (UITableViewCellAccessoryType)tableViewCellAccessoryTypeWithStaticAccessoryType:(QMUIStaticTableViewCellAccessoryType)type; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.m b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.m index cde445d1..341f5432 100644 --- a/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.m +++ b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIStaticTableViewCellData.m @@ -30,19 +30,19 @@ + (instancetype)staticTableViewCellDataWithIdentifier:(NSInteger)identifier didSelectTarget:(id)didSelectTarget didSelectAction:(SEL)didSelectAction accessoryType:(QMUIStaticTableViewCellAccessoryType)accessoryType { - return [QMUIStaticTableViewCellData staticTableViewCellDataWithIdentifier:identifier - cellClass:[QMUITableViewCell class] - style:UITableViewCellStyleDefault - height:TableViewCellNormalHeight - image:image - text:text - detailText:detailText - didSelectTarget:didSelectTarget - didSelectAction:didSelectAction - accessoryType:accessoryType - accessoryValueObject:nil - accessoryTarget:nil - accessoryAction:NULL]; + return [self staticTableViewCellDataWithIdentifier:identifier + cellClass:[QMUITableViewCell class] + style:UITableViewCellStyleDefault + height:TableViewCellNormalHeight + image:image + text:text + detailText:detailText + didSelectTarget:didSelectTarget + didSelectAction:didSelectAction + accessoryType:accessoryType + accessoryValueObject:nil + accessoryTarget:nil + accessoryAction:NULL]; } + (instancetype)staticTableViewCellDataWithIdentifier:(NSInteger)identifier @@ -58,7 +58,7 @@ + (instancetype)staticTableViewCellDataWithIdentifier:(NSInteger)identifier accessoryValueObject:(NSObject *)accessoryValueObject accessoryTarget:(id)accessoryTarget accessoryAction:(SEL)accessoryAction { - QMUIStaticTableViewCellData *data = [[QMUIStaticTableViewCellData alloc] init]; + QMUIStaticTableViewCellData *data = [[self alloc] init]; data.identifier = identifier; data.cellClass = cellClass; data.style = style; @@ -84,7 +84,7 @@ - (instancetype)init { } - (void)setCellClass:(Class)cellClass { - NSAssert([cellClass isSubclassOfClass:[QMUITableViewCell class]], @"%@.cellClass 必须为 QMUITableViewCell 的子类", NSStringFromClass(self.class)); + QMUIAssert([cellClass isSubclassOfClass:[QMUITableViewCell class]], NSStringFromClass(self.class), @"%@.cellClass 必须为 QMUITableViewCell 的子类", NSStringFromClass(self.class)); _cellClass = cellClass; } diff --git a/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.h b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.h index d1e9295c..3f225cac 100644 --- a/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.h +++ b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIStaticTableViewCellDataSource.h diff --git a/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m index 8d39c79f..ccb14970 100644 --- a/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m +++ b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIStaticTableViewCellDataSource.m @@ -18,9 +18,9 @@ #import "QMUIStaticTableViewCellData.h" #import "QMUITableViewCell.h" #import "UITableView+QMUIStaticCell.h" -#import #import "QMUILog.h" #import "QMUIMultipleDelegates.h" +#import "NSArray+QMUI.h" @interface QMUIStaticTableViewCellDataSource () @end @@ -41,6 +41,11 @@ - (instancetype)initWithCellDataSections:(NSArray *> *)cellDataSections { +#ifdef DEBUG + [cellDataSections qmui_enumerateNestedArrayWithBlock:^(QMUIStaticTableViewCellData *obj, BOOL * _Nonnull stop) { + QMUIAssert([obj isKindOfClass:QMUIStaticTableViewCellData.class], NSStringFromClass(self.class), @"cellDataSections 内只允许出现 QMUIStatictableViewCellData 类型的元素"); + }]; +#endif _cellDataSections = cellDataSections; [self.tableView reloadData]; } @@ -49,8 +54,8 @@ - (void)setCellDataSections:(NSArray *> * - (void)setTableView:(UITableView *)tableView { _tableView = tableView; // 触发 UITableView (QMUI_StaticCell) 里重写的 setter 里的逻辑 - tableView.delegate = tableView.delegate; tableView.dataSource = tableView.dataSource; + tableView.delegate = tableView.delegate; } @end @@ -116,12 +121,16 @@ - (QMUITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath { } switcher.on = switcherOn; [switcher removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents]; - [switcher addTarget:data.accessoryTarget action:data.accessoryAction forControlEvents:UIControlEventValueChanged]; + if (data.accessorySwitchBlock) { + [switcher addTarget:self action:@selector(handleSwitcherEvent:) forControlEvents:UIControlEventValueChanged]; + } else if ([data.accessoryTarget respondsToSelector:data.accessoryAction]) { + [switcher addTarget:data.accessoryTarget action:data.accessoryAction forControlEvents:UIControlEventValueChanged]; + } cell.accessoryView = switcher; } // 统一设置selectionStyle - if (data.accessoryType == QMUIStaticTableViewCellAccessoryTypeSwitch || (!data.didSelectTarget || !data.didSelectAction)) { + if (data.accessoryType == QMUIStaticTableViewCellAccessoryTypeSwitch || (!data.didSelectBlock && (!data.didSelectTarget || !data.didSelectAction))) { cell.selectionStyle = UITableViewCellSelectionStyleNone; } else { cell.selectionStyle = UITableViewCellSelectionStyleBlue; @@ -143,7 +152,7 @@ - (CGFloat)heightForRowAtIndexPath:(NSIndexPath *)indexPath { - (void)didSelectRowAtIndexPath:(NSIndexPath *)indexPath { QMUIStaticTableViewCellData *cellData = [self cellDataAtIndexPath:indexPath]; - if (!cellData || !cellData.didSelectTarget || !cellData.didSelectAction) { + if (!cellData || (!cellData.didSelectBlock && (!cellData.didSelectTarget || !cellData.didSelectAction))) { QMUITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; if (cell.selectionStyle != UITableViewCellSelectionStyleNone) { [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; @@ -152,10 +161,14 @@ - (void)didSelectRowAtIndexPath:(NSIndexPath *)indexPath { } // 1、分发选中事件(UISwitch 类型不支持 didSelect) - if ([cellData.didSelectTarget respondsToSelector:cellData.didSelectAction] && cellData.accessoryType != QMUIStaticTableViewCellAccessoryTypeSwitch) { - BeginIgnorePerformSelectorLeaksWarning - [cellData.didSelectTarget performSelector:cellData.didSelectAction withObject:cellData]; - EndIgnorePerformSelectorLeaksWarning + if (cellData.accessoryType != QMUIStaticTableViewCellAccessoryTypeSwitch) { + if (cellData.didSelectBlock) { + cellData.didSelectBlock(self.tableView, cellData); + } else if ([cellData.didSelectTarget respondsToSelector:cellData.didSelectAction]) { + BeginIgnorePerformSelectorLeaksWarning + [cellData.didSelectTarget performSelector:cellData.didSelectAction withObject:cellData]; + EndIgnorePerformSelectorLeaksWarning + } } // 2、处理点击状态(对checkmark类型的cell,选中后自动反选) @@ -166,11 +179,21 @@ - (void)didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - (void)accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath { QMUIStaticTableViewCellData *cellData = [self cellDataAtIndexPath:indexPath]; - if ([cellData.accessoryTarget respondsToSelector:cellData.accessoryAction]) { + if (cellData.accessoryBlock) { + cellData.accessoryBlock(self.tableView, cellData); + } else if ([cellData.accessoryTarget respondsToSelector:cellData.accessoryAction]) { BeginIgnorePerformSelectorLeaksWarning [cellData.accessoryTarget performSelector:cellData.accessoryAction withObject:cellData]; EndIgnorePerformSelectorLeaksWarning } } +- (void)handleSwitcherEvent:(UISwitch *)swicher { + NSIndexPath *indexPath = [self.tableView qmui_indexPathForRowAtView:swicher]; + QMUIStaticTableViewCellData *cellData = [self cellDataAtIndexPath:indexPath]; + if (cellData.accessorySwitchBlock) { + cellData.accessorySwitchBlock(self.tableView, cellData, swicher); + } +} + @end diff --git a/QMUI/QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.h b/QMUI/QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.h index 0595a89d..643b37b5 100644 --- a/QMUI/QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.h +++ b/QMUI/QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITableView+QMUIStaticCell.h diff --git a/QMUI/QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.m b/QMUI/QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.m index 7d2f33f5..a63841be 100644 --- a/QMUI/QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.m +++ b/QMUI/QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITableView+QMUIStaticCell.m @@ -16,7 +16,6 @@ #import "UITableView+QMUIStaticCell.h" #import "QMUICore.h" #import "QMUIStaticTableViewCellDataSource.h" -#import #import "QMUILog.h" #import "QMUIMultipleDelegates.h" @@ -33,26 +32,23 @@ + (void)load { OverrideImplementation([UITableView class], @selector(setDataSource:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UITableView *selfObject, id dataSource) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (dataSource && selfObject.qmui_staticCellDataSource) { - void (^addSelectorBlock)(id) = ^void(id aDataSource) { - // 这些 addMethod 的操作必须要在系统的 setDataSource 执行前就执行,否则 tableView 可能会认为不存在这些 method - // 并且 addMethod 操作执行一次之后,直到 App 进程被杀死前都会生效,所以多次进入这段代码可能就会提示添加方法失败,请不用在意 - [selfObject addSelector:@selector(numberOfSectionsInTableView:) withImplementation:(IMP)staticCell_numberOfSections types:"l@:@" forObject:aDataSource]; - [selfObject addSelector:@selector(tableView:numberOfRowsInSection:) withImplementation:(IMP)staticCell_numberOfRows types:"l@:@l" forObject:aDataSource]; - [selfObject addSelector:@selector(tableView:cellForRowAtIndexPath:) withImplementation:(IMP)staticCell_cellForRow types:"@@:@@" forObject:aDataSource]; - }; - if ([dataSource isKindOfClass:[QMUIMultipleDelegates class]]) { - NSPointerArray *delegates = [((QMUIMultipleDelegates *)dataSource).delegates copy]; - for (id delegate in delegates) { - if ([delegate conformsToProtocol:@protocol(UITableViewDataSource)]) { - addSelectorBlock((id)delegate); - } + if (dataSource && selfObject.qmui_staticCellDataSource) { + void (^addSelectorBlock)(id) = ^void(id aDataSource) { + // 这些 addMethod 的操作必须要在系统的 setDataSource 执行前就执行,否则 tableView 可能会认为不存在这些 method + // 并且 addMethod 操作执行一次之后,直到 App 进程被杀死前都会生效,所以多次进入这段代码可能就会提示添加方法失败,请不用在意 + [selfObject addSelector:@selector(numberOfSectionsInTableView:) withImplementation:(IMP)staticCell_numberOfSections types:"l@:@" forObject:aDataSource]; + [selfObject addSelector:@selector(tableView:numberOfRowsInSection:) withImplementation:(IMP)staticCell_numberOfRows types:"l@:@l" forObject:aDataSource]; + [selfObject addSelector:@selector(tableView:cellForRowAtIndexPath:) withImplementation:(IMP)staticCell_cellForRow types:"@@:@@" forObject:aDataSource]; + }; + if ([dataSource isKindOfClass:[QMUIMultipleDelegates class]]) { + NSPointerArray *delegates = [((QMUIMultipleDelegates *)dataSource).delegates copy]; + for (id delegate in delegates) { + if ([delegate conformsToProtocol:@protocol(UITableViewDataSource)]) { + addSelectorBlock((id)delegate); } - } else { - addSelectorBlock((id)dataSource); } + } else { + addSelectorBlock((id)dataSource); } } @@ -66,26 +62,23 @@ + (void)load { OverrideImplementation([UITableView class], @selector(setDelegate:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UITableView *selfObject, id delegate) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (delegate && selfObject.qmui_staticCellDataSource) { - void (^addSelectorBlock)(id) = ^void(id aDelegate) { - // 这些 addMethod 的操作必须要在系统的 setDelegate 执行前就执行,否则 tableView 可能会认为不存在这些 method - // 并且 addMethod 操作执行一次之后,直到 App 进程被杀死前都会生效,所以多次进入这段代码可能就会提示添加方法失败,请不用在意 - [selfObject addSelector:@selector(tableView:heightForRowAtIndexPath:) withImplementation:(IMP)staticCell_heightForRow types:"d@:@@" forObject:aDelegate]; - [selfObject addSelector:@selector(tableView:didSelectRowAtIndexPath:) withImplementation:(IMP)staticCell_didSelectRow types:"v@:@@" forObject:aDelegate]; - [selfObject addSelector:@selector(tableView:accessoryButtonTappedForRowWithIndexPath:) withImplementation:(IMP)staticCell_accessoryButtonTapped types:"v@:@@" forObject:aDelegate]; - }; - if ([delegate isKindOfClass:[QMUIMultipleDelegates class]]) { - NSPointerArray *delegates = [((QMUIMultipleDelegates *)delegate).delegates copy]; - for (id d in delegates) { - if ([d conformsToProtocol:@protocol(UITableViewDelegate)]) { - addSelectorBlock((id)d); - } + if (delegate && selfObject.qmui_staticCellDataSource) { + void (^addSelectorBlock)(id) = ^void(id aDelegate) { + // 这些 addMethod 的操作必须要在系统的 setDelegate 执行前就执行,否则 tableView 可能会认为不存在这些 method + // 并且 addMethod 操作执行一次之后,直到 App 进程被杀死前都会生效,所以多次进入这段代码可能就会提示添加方法失败,请不用在意 + [selfObject addSelector:@selector(tableView:heightForRowAtIndexPath:) withImplementation:(IMP)staticCell_heightForRow types:"d@:@@" forObject:aDelegate]; + [selfObject addSelector:@selector(tableView:didSelectRowAtIndexPath:) withImplementation:(IMP)staticCell_didSelectRow types:"v@:@@" forObject:aDelegate]; + [selfObject addSelector:@selector(tableView:accessoryButtonTappedForRowWithIndexPath:) withImplementation:(IMP)staticCell_accessoryButtonTapped types:"v@:@@" forObject:aDelegate]; + }; + if ([delegate isKindOfClass:[QMUIMultipleDelegates class]]) { + NSPointerArray *delegates = [((QMUIMultipleDelegates *)delegate).delegates copy]; + for (id d in delegates) { + if ([d conformsToProtocol:@protocol(UITableViewDelegate)]) { + addSelectorBlock((id)d); } - } else { - addSelectorBlock((id)delegate); } + } else { + addSelectorBlock((id)delegate); } } @@ -109,19 +102,12 @@ - (QMUIStaticTableViewCellDataSource *)qmui_staticCellDataSource { return (QMUIStaticTableViewCellDataSource *)objc_getAssociatedObject(self, &kAssociatedObjectKey_staticCellDataSource); } -// 把那些已经手动 addMethod 过的 class 存起来,避免每次都触发 log,打了一堆重复的信息 -static NSMutableSet *QMUI_staticTableViewAddedClass; - - (void)addSelector:(SEL)selector withImplementation:(IMP)implementation types:(const char *)types forObject:(NSObject *)object { if (!class_addMethod(object.class, selector, implementation, types)) { - if (!QMUI_staticTableViewAddedClass) { - QMUI_staticTableViewAddedClass = [[NSMutableSet alloc] init]; - } - NSString *identifier = [NSString stringWithFormat:@"%@%@", NSStringFromClass(object.class), NSStringFromSelector(selector)]; - if (![QMUI_staticTableViewAddedClass containsObject:identifier]) { + // 把那些已经手动 addMethod 过的 class 存起来,避免每次都触发 log,打了一堆重复的信息 + [QMUIHelper executeBlock:^{ QMUILog(NSStringFromClass(self.class), @"尝试为 %@ 添加方法 %@ 失败,可能该类里已经实现了这个方法", NSStringFromClass(object.class), NSStringFromSelector(selector)); - [QMUI_staticTableViewAddedClass addObject:identifier]; - } + } oncePerIdentifier:[NSString stringWithFormat:@"addedlog %@-%@", NSStringFromClass(object.class), NSStringFromSelector(selector)]]; } } diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.h b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.h index 3749a275..d8be3f04 100644 --- a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.h +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIToastAnimator.h @@ -30,9 +30,6 @@ - (BOOL)isAnimating; @end - -// TODO: 实现多种animation类型 - typedef NS_ENUM(NSInteger, QMUIToastAnimationType) { QMUIToastAnimationTypeFade = 0, QMUIToastAnimationTypeZoom, diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.m b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.m index 95f7b2fa..e9f41818 100644 --- a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.m +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIToastAnimator.m @@ -17,10 +17,14 @@ #import "QMUICore.h" #import "QMUIToastView.h" -@interface QMUIToastAnimator () +#define kSlideAnimationKey @"kSlideAnimationKey" + +@interface QMUIToastAnimator () + +@property (nonatomic, assign) BOOL isShowing; +@property (nonatomic, assign) BOOL isAnimating; +@property (nonatomic, copy) void (^basicAnimationCompletion)(BOOL finished); -@property(nonatomic, assign) BOOL isShowing; -@property(nonatomic, assign) BOOL isAnimating; @end @implementation QMUIToastAnimator @@ -45,11 +49,62 @@ - (instancetype)initWithToastView:(QMUIToastView *)toastView { - (void)showWithCompletion:(void (^)(BOOL finished))completion { self.isShowing = YES; + switch (self.animationType) { + case QMUIToastAnimationTypeZoom:{ + [self zoomAnimationForShow:YES withCompletion:completion]; + } + break; + case QMUIToastAnimationTypeSlide:{ + [self slideAnimationForShow:YES withCompletion:completion]; + } + break; + case QMUIToastAnimationTypeFade: + default:{ + [self fadeAnimationForShow:YES withCompletion:completion]; + } + break; + } +} + +- (void)hideWithCompletion:(void (^)(BOOL finished))completion { + self.isShowing = NO; + switch (self.animationType) { + case QMUIToastAnimationTypeZoom:{ + [self zoomAnimationForShow:NO withCompletion:completion]; + } + break; + case QMUIToastAnimationTypeSlide:{ + [self slideAnimationForShow:NO withCompletion:completion]; + } + break; + case QMUIToastAnimationTypeFade: + default:{ + [self fadeAnimationForShow:NO withCompletion:completion]; + } + break; + } +} + +- (void)zoomAnimationForShow:(BOOL)show withCompletion:(void (^)(BOOL))completion { + CGFloat alpha = show ? 1.f : 0.f; + CGAffineTransform small = CGAffineTransformMakeScale(0.5f, 0.5f); + CGAffineTransform endTransform = show ? CGAffineTransformIdentity : small; self.isAnimating = YES; - [UIView animateWithDuration:0.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut|UIViewAnimationOptionBeginFromCurrentState animations:^{ - self.toastView.backgroundView.alpha = 1.0; - self.toastView.contentView.alpha = 1.0; + if (show) { + self.toastView.backgroundView.transform = small; + self.toastView.contentView.transform = small; + } + [UIView animateWithDuration:0.25 + delay:0.0 + options:QMUIViewAnimationOptionsCurveOut|UIViewAnimationOptionBeginFromCurrentState + animations:^{ + self.toastView.backgroundView.alpha = alpha; + self.toastView.contentView.alpha = alpha; + self.toastView.backgroundView.transform = endTransform; + self.toastView.contentView.transform = endTransform; } completion:^(BOOL finished) { + self.toastView.backgroundView.transform = endTransform; + self.toastView.contentView.transform = endTransform; self.isAnimating = NO; if (completion) { completion(finished); @@ -57,12 +112,27 @@ - (void)showWithCompletion:(void (^)(BOOL finished))completion { }]; } -- (void)hideWithCompletion:(void (^)(BOOL finished))completion { - self.isShowing = NO; +- (void)slideAnimationForShow:(BOOL)show withCompletion:(void (^)(BOOL))completion { + self.basicAnimationCompletion = [completion copy]; + self.isAnimating = YES; + if (show) { + [self showSlideAnimationOnView:self.toastView.backgroundView withIndentifier:@"showBackgroundView"]; + [self showSlideAnimationOnView:self.toastView.contentView withIndentifier:@"showContentView"]; + }else{ + [self hideSlideAnimationOnView:self.toastView.backgroundView withIndentifier:@"hideBackgroundView"]; + [self hideSlideAnimationOnView:self.toastView.contentView withIndentifier:@"hideContentView"]; + } +} + +- (void)fadeAnimationForShow:(BOOL)show withCompletion:(void (^)(BOOL))completion { + CGFloat alpha = show ? 1.f : 0.f; self.isAnimating = YES; - [UIView animateWithDuration:0.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut|UIViewAnimationOptionBeginFromCurrentState animations:^{ - self.toastView.backgroundView.alpha = 0.0; - self.toastView.contentView.alpha = 0.0; + [UIView animateWithDuration:0.25 + delay:0.0 + options:QMUIViewAnimationOptionsCurveOut|UIViewAnimationOptionBeginFromCurrentState + animations:^{ + self.toastView.backgroundView.alpha = alpha; + self.toastView.contentView.alpha = alpha; } completion:^(BOOL finished) { self.isAnimating = NO; if (completion) { @@ -71,6 +141,87 @@ - (void)hideWithCompletion:(void (^)(BOOL finished))completion { }]; } +- (void)showSlideAnimationOnView:(UIView *)popupView withIndentifier:(NSString *)key { + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.translation.y"]; + animation.fromValue = [NSNumber numberWithFloat:- [[UIScreen mainScreen] bounds].size.height / 2 - popupView.frame.size.height / 2]; + animation.toValue = [NSNumber numberWithFloat:0]; + animation.duration = 0.6; + animation.delegate = self; + animation.removedOnCompletion = NO; + animation.fillMode = kCAFillModeBoth; + animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.51 : 1.24 : 0.02 : 0.99]; + [animation setValue:key forKey:kSlideAnimationKey]; + [popupView.layer addAnimation:animation forKey:@"showPopupView"]; + + CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; + opacityAnimation.fromValue = [NSNumber numberWithFloat:0.0]; + opacityAnimation.toValue = [NSNumber numberWithFloat:1]; + opacityAnimation.duration = 0.27; + opacityAnimation.beginTime=CACurrentMediaTime() + 0.03; + opacityAnimation.removedOnCompletion = NO; + opacityAnimation.fillMode = kCAFillModeBoth; + opacityAnimation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.25 : 0.1: 0.25 : 1]; + + [popupView.layer addAnimation:opacityAnimation forKey:@"showOpacityKey"]; + + CABasicAnimation *rotateAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; + rotateAnimation.fromValue = [NSNumber numberWithFloat:2 * M_PI/180]; + rotateAnimation.toValue = [NSNumber numberWithFloat:0]; + rotateAnimation.duration = 0.17; + rotateAnimation.beginTime=CACurrentMediaTime() + 0.26; + rotateAnimation.removedOnCompletion = NO; + rotateAnimation.fillMode = kCAFillModeBoth; + rotateAnimation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.25 : 0.1 : 0.25 : 1]; + + [popupView.layer addAnimation:rotateAnimation forKey:@"showRotateKey"]; +} + +- (void)hideSlideAnimationOnView:(UIView *)popupView withIndentifier:(NSString *)key { + CABasicAnimation *animationY = [CABasicAnimation animationWithKeyPath:@"transform.translation.y"]; + animationY.fromValue = [NSNumber numberWithFloat:0]; + animationY.toValue = [NSNumber numberWithFloat:[[UIScreen mainScreen] bounds].size.height/2+popupView.frame.size.height/2]; + animationY.duration = 0.7; + animationY.removedOnCompletion = NO; + animationY.fillMode = kCAFillModeBoth; + animationY.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.73 : -0.38 : 0.03 : 1.41]; + animationY.delegate = self; + [animationY setValue:key forKey:kSlideAnimationKey]; + [popupView.layer addAnimation:animationY forKey:@"hidePopupView"]; + + CABasicAnimation *rotateAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; + rotateAnimation.fromValue = [NSNumber numberWithFloat:0]; + rotateAnimation.toValue = [NSNumber numberWithFloat:3 * M_PI/180]; + rotateAnimation.duration = 0.4; + rotateAnimation.beginTime=CACurrentMediaTime() + 0.05; + rotateAnimation.removedOnCompletion = NO; + rotateAnimation.fillMode = kCAFillModeBoth; + rotateAnimation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.25 : 0.1 : 0.25 : 1]; + + [popupView.layer addAnimation:rotateAnimation forKey:@"hideRotateKey"]; + + + CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; + opacityAnimation.fromValue = [NSNumber numberWithFloat:1]; + opacityAnimation.toValue = [NSNumber numberWithFloat:0]; + opacityAnimation.duration = 0.25; + opacityAnimation.beginTime=CACurrentMediaTime() + 0.15; + opacityAnimation.removedOnCompletion = NO; + opacityAnimation.fillMode = kCAFillModeBoth; + opacityAnimation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.53 : 0.92 : 1 : 1]; + + [popupView.layer addAnimation:opacityAnimation forKey:@"hideOpacityKey"]; +} + +- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag { + if([[animation valueForKey:kSlideAnimationKey] isEqual:@"showContentView"] || + [[animation valueForKey:kSlideAnimationKey] isEqual:@"hideContentView"]) { + if (self.basicAnimationCompletion) { + self.basicAnimationCompletion(flag); + } + self.isAnimating = NO; + } +} + - (BOOL)isShowing { return self.isShowing; } diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.h b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.h index 1721eebc..e1a8f818 100644 --- a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.h +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIToastBackgroundView.h @@ -15,8 +15,6 @@ #import -@class QMUIVisualEffectView; - @interface QMUIToastBackgroundView : UIView /** @@ -24,10 +22,10 @@ */ @property(nonatomic, assign) BOOL shouldBlurBackgroundView; -@property(nullable, nonatomic, strong, readonly) QMUIVisualEffectView *effectView; +@property(nullable, nonatomic, strong, readonly) UIVisualEffectView *effectView; /** - * 如果不设置磨砂,则styleColor直接作为`QMUIToastBackgroundView`的backgroundColor;如果需要磨砂,则会新增加一个`UIVisualEffectView`放在`QMUIToastBackgroundView`上面 + * 如果不设置磨砂,则styleColor直接作为`QMUIToastBackgroundView`的backgroundColor;如果需要磨砂,则会新增加一个`UIVisualEffectView`放在`QMUIToastBackgroundView`上面。 */ @property(nullable, nonatomic, strong) UIColor *styleColor UI_APPEARANCE_SELECTOR; diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.m b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.m index aaff4b52..4a77c457 100644 --- a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.m +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIToastBackgroundView.m @@ -15,7 +15,6 @@ #import "QMUIToastBackgroundView.h" #import "QMUICore.h" -#import "QMUIVisualEffectView.h" @interface QMUIToastBackgroundView () @@ -38,10 +37,9 @@ - (void)setShouldBlurBackgroundView:(BOOL)shouldBlurBackgroundView { _shouldBlurBackgroundView = shouldBlurBackgroundView; if (shouldBlurBackgroundView) { UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; - _effectView = [[QMUIVisualEffectView alloc] initWithEffect:effect]; + _effectView = [[UIVisualEffectView alloc] initWithEffect:effect]; self.effectView.layer.cornerRadius = self.cornerRadius; self.effectView.layer.masksToBounds = YES; - self.effectView.foregroundColor = nil; [self addSubview:self.effectView]; } else { if (self.effectView) { diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.h b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.h index 9fa42fff..e57b549b 100644 --- a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.h +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIToastContentView.h @@ -24,12 +24,12 @@ @interface QMUIToastContentView : UIView /** - * 设置一个UIView,可以是:菊花、图片等等,请自行保证 customView 的 size 被正确设置 + * 设置一个UIView,可以是:菊花、图片等等,请自行保证 customView 的 size 被正确设置。 */ @property(nonatomic, strong) UIView *customView; /** - * 设置第一行大文字label + * 设置第一行大文字label。 */ @property(nonatomic, strong, readonly) UILabel *textLabel; @@ -39,7 +39,7 @@ @property(nonatomic, copy) NSString *textLabelText; /** - * 设置第二行小文字label + * 设置第二行小文字label。 */ @property(nonatomic, strong, readonly) UILabel *detailTextLabel; @@ -59,27 +59,27 @@ @property(nonatomic, assign) CGSize minimumSize UI_APPEARANCE_SELECTOR; /** - * 设置customView的marginBottom + * 设置customView的marginBottom。 */ @property(nonatomic, assign) CGFloat customViewMarginBottom UI_APPEARANCE_SELECTOR; /** - * 设置textLabel的marginBottom + * 设置textLabel的marginBottom。 */ @property(nonatomic, assign) CGFloat textLabelMarginBottom UI_APPEARANCE_SELECTOR; /** - * 设置detailTextLabel的marginBottom + * 设置detailTextLabel的marginBottom。 */ @property(nonatomic, assign) CGFloat detailTextLabelMarginBottom UI_APPEARANCE_SELECTOR; /** - * 设置textLabel的attributes,如果包含 NSForegroundColorAttributeName 则 textLabel 不响应 tintColor,如果不包含则 textLabel 会拿 tintColor 当成文字颜色 + * 设置textLabel的attributes,如果包含 NSForegroundColorAttributeName 则 textLabel 不响应 tintColor,如果不包含则 textLabel 会拿 tintColor 当成文字颜色。 */ @property(nonatomic, strong) NSDictionary *textLabelAttributes UI_APPEARANCE_SELECTOR; /** - * 设置 detailTextLabel 的 attributes,如果包含 NSForegroundColorAttributeName 则 detailTextLabel 不响应 tintColor,如果不包含则 detailTextLabel 会拿 tintColor 当成文字颜色 + * 设置 detailTextLabel 的 attributes,如果包含 NSForegroundColorAttributeName 则 detailTextLabel 不响应 tintColor,如果不包含则 detailTextLabel 会拿 tintColor 当成文字颜色。 */ @property(nonatomic, strong) NSDictionary *detailTextLabelAttributes UI_APPEARANCE_SELECTOR; diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.m b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.m index 9b811d9b..4e3c5afb 100644 --- a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.m +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIToastContentView.m @@ -50,7 +50,7 @@ - (void)setCustomView:(UIView *)customView { _customView = customView; [self addSubview:self.customView]; [self updateCustomViewTintColor]; - [self setNeedsLayout]; + [self.superview setNeedsLayout]; } - (void)setTextLabelText:(NSString *)textLabelText { @@ -58,8 +58,8 @@ - (void)setTextLabelText:(NSString *)textLabelText { if (textLabelText) { self.textLabel.attributedText = [[NSAttributedString alloc] initWithString:textLabelText attributes:self.textLabelAttributes]; self.textLabel.textAlignment = NSTextAlignmentCenter; - [self setNeedsLayout]; } + [self.superview setNeedsLayout]; } - (void)setDetailTextLabelText:(NSString *)detailTextLabelText { @@ -68,6 +68,7 @@ - (void)setDetailTextLabelText:(NSString *)detailTextLabelText { self.detailTextLabel.attributedText = [[NSAttributedString alloc] initWithString:detailTextLabelText attributes:self.detailTextLabelAttributes]; self.detailTextLabel.textAlignment = NSTextAlignmentCenter; } + [self.superview setNeedsLayout]; } - (CGSize)sizeThatFits:(CGSize)size { @@ -177,27 +178,27 @@ - (void)updateCustomViewTintColor { - (void)setInsets:(UIEdgeInsets)insets { _insets = insets; - [self setNeedsLayout]; + [self.superview setNeedsLayout]; } - (void)setMinimumSize:(CGSize)minimumSize { _minimumSize = minimumSize; - [self setNeedsLayout]; + [self.superview setNeedsLayout]; } - (void)setCustomViewMarginBottom:(CGFloat)customViewMarginBottom { _customViewMarginBottom = customViewMarginBottom; - [self setNeedsLayout]; + [self.superview setNeedsLayout]; } - (void)setTextLabelMarginBottom:(CGFloat)textLabelMarginBottom { _textLabelMarginBottom = textLabelMarginBottom; - [self setNeedsLayout]; + [self.superview setNeedsLayout]; } - (void)setDetailTextLabelMarginBottom:(CGFloat)detailTextLabelMarginBottom { _detailTextLabelMarginBottom = detailTextLabelMarginBottom; - [self setNeedsLayout]; + [self.superview setNeedsLayout]; } - (void)setTextLabelAttributes:(NSDictionary *)textLabelAttributes { @@ -206,6 +207,7 @@ - (void)setTextLabelAttributes:(NSDictionary *)textLabelAttributes { // 刷新label的attributes self.textLabelText = self.textLabelText; } + [self.superview setNeedsLayout]; } - (void)setDetailTextLabelAttributes:(NSDictionary *)detailTextLabelAttributes { @@ -214,6 +216,7 @@ - (void)setDetailTextLabelAttributes:(NSDictionary *)detailTextLabelAttributes { // 刷新label的attributes self.detailTextLabelText = self.detailTextLabelText; } + [self.superview setNeedsLayout]; } @end diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastView.h b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastView.h index 73ce0e27..61123762 100644 --- a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastView.h +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastView.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIToastView.h @@ -111,7 +111,7 @@ typedef NS_ENUM(NSInteger, QMUIToastViewPosition) { /** * 会盖住整个superView,防止手指可以点击到ToastView下面的内容,默认透明。 */ -@property(nonatomic, strong, readonly) UIView *maskView; +@property(nonatomic, strong, readonly) UIView *dimmingView; /**s * 承载Toast内容的UIView,可以自定义并赋值给contentView。如果contentView需要跟随ToastView的tintColor变化而变化,可以重写自定义view的`tintColorDidChange`来实现。默认使用`QMUIToastContentView`实现。 diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastView.m b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastView.m index 1e1cd6ee..af83e6f2 100644 --- a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastView.m +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastView.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIToastView.m @@ -74,9 +74,9 @@ - (void)didInitialize { self.backgroundColor = UIColorClear; self.layer.allowsGroupOpacity = NO; - _maskView = [[UIView alloc] init]; - self.maskView.backgroundColor = UIColorClear; - [self addSubview:self.maskView]; + _dimmingView = [[UIView alloc] init]; + self.dimmingView.backgroundColor = UIColorClear; + [self addSubview:self.dimmingView]; [self registerNotifications]; } @@ -144,12 +144,12 @@ - (void)layoutSubviews { [super layoutSubviews]; self.frame = self.parentView.bounds; - self.maskView.frame = self.bounds; + self.dimmingView.frame = self.bounds; CGFloat contentWidth = CGRectGetWidth(self.parentView.bounds); CGFloat contentHeight = CGRectGetHeight(self.parentView.bounds); - UIEdgeInsets marginInsets = UIEdgeInsetsConcat(self.marginInsets, self.parentView.qmui_safeAreaInsets); + UIEdgeInsets marginInsets = UIEdgeInsetsConcat(self.marginInsets, self.parentView.safeAreaInsets); CGFloat limitWidth = contentWidth - UIEdgeInsetsGetHorizontalValue(marginInsets); CGFloat limitHeight = contentHeight - UIEdgeInsetsGetVerticalValue(marginInsets); @@ -179,6 +179,7 @@ - (void)layoutSubviews { CGRect contentRect = CGRectFlatMake(contentViewX, contentViewY, contentViewSize.width, contentViewSize.height); self.contentView.qmui_frameApplyTransform = contentRect; + [self.contentView setNeedsLayout]; } if (self.backgroundView) { // backgroundView的frame跟contentView一样,contentView里面的subviews如果需要在视觉上跟backgroundView有个padding,那么就自己在自定义的contentView里面做。 diff --git a/QMUI/QMUIKit/QMUICore/QMUICommonDefines.h b/QMUI/QMUIKit/QMUICore/QMUICommonDefines.h index dba713c5..8f77b6d4 100644 --- a/QMUI/QMUIKit/QMUICore/QMUICommonDefines.h +++ b/QMUI/QMUIKit/QMUICore/QMUICommonDefines.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUICommonDefines.h @@ -18,6 +18,7 @@ #import #import "QMUIHelper.h" +#import "NSString+QMUI.h" #pragma mark - 变量-编译相关 @@ -28,6 +29,8 @@ #define IS_DEBUG NO #endif +#define IS_XCTEST (!!NSProcessInfo.processInfo.environment[@"XCTestConfigurationFilePath"]) + #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 /// 当前编译使用的 Base SDK 版本为 iOS 9.0 及以上 #define IOS9_SDK_ALLOWED YES @@ -48,6 +51,36 @@ #define IOS12_SDK_ALLOWED YES #endif +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 +/// 当前编译使用的 Base SDK 版本为 iOS 13.0 及以上 +#define IOS13_SDK_ALLOWED YES +#endif + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 +/// 当前编译使用的 Base SDK 版本为 iOS 14.0 及以上 +#define IOS14_SDK_ALLOWED YES +#endif + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000 +/// 当前编译使用的 Base SDK 版本为 iOS 15.0 及以上 +#define IOS15_SDK_ALLOWED YES +#endif + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 160000 +/// 当前编译使用的 Base SDK 版本为 iOS 16.0 及以上 +#define IOS16_SDK_ALLOWED YES +#endif + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 170000 +/// 当前编译使用的 Base SDK 版本为 iOS 17.0 及以上 +#define IOS17_SDK_ALLOWED YES +#endif + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 180000 +/// 当前编译使用的 Base SDK 版本为 iOS 18.0 及以上 +#define IOS18_SDK_ALLOWED YES +#endif + #pragma mark - Clang #define ArgumentToString(macro) #macro @@ -66,6 +99,12 @@ #define BeginIgnoreDeprecatedWarning BeginIgnoreClangWarning(-Wdeprecated-declarations) #define EndIgnoreDeprecatedWarning EndIgnoreClangWarning +#pragma mark - 忽略 iOS 13 KVC 访问私有属性限制 + +/// 将 KVC 代码包裹在这个宏中,可忽略系统的 KVC 访问限制 +#define BeginIgnoreUIKVCAccessProhibited NSThread.currentThread.qmui_shouldIgnoreUIKVCAccessProhibited = YES; +#define EndIgnoreUIKVCAccessProhibited NSThread.currentThread.qmui_shouldIgnoreUIKVCAccessProhibited = NO; + #pragma mark - 变量-设备相关 /// 设备类型 @@ -73,6 +112,7 @@ #define IS_IPOD [QMUIHelper isIPod] #define IS_IPHONE [QMUIHelper isIPhone] #define IS_SIMULATOR [QMUIHelper isSimulator] +#define IS_MAC [QMUIHelper isMac] /// 操作系统版本号,只获取第二级的版本号,例如 10.3.1 只会得到 10.3 #define IOS_VERSION ([[[UIDevice currentDevice] systemVersion] doubleValue]) @@ -82,7 +122,7 @@ /// 是否横竖屏 /// 用户界面横屏了才会返回YES -#define IS_LANDSCAPE UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation]) +#define IS_LANDSCAPE UIInterfaceOrientationIsLandscape(UIApplication.sharedApplication.statusBarOrientation) /// 无论支不支持横屏,只要设备横屏了,就会返回YES #define IS_DEVICE_LANDSCAPE UIDeviceOrientationIsLandscape([[UIDevice currentDevice] orientation]) @@ -92,22 +132,38 @@ /// 屏幕高度,会根据横竖屏的变化而变化 #define SCREEN_HEIGHT ([[UIScreen mainScreen] bounds].size.height) -/// 屏幕宽度,跟横竖屏无关 -#define DEVICE_WIDTH (IS_LANDSCAPE ? [[UIScreen mainScreen] bounds].size.height : [[UIScreen mainScreen] bounds].size.width) +/// 设备宽度,跟横竖屏无关 +#define DEVICE_WIDTH MIN([[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height) -/// 屏幕高度,跟横竖屏无关 -#define DEVICE_HEIGHT (IS_LANDSCAPE ? [[UIScreen mainScreen] bounds].size.width : [[UIScreen mainScreen] bounds].size.height) +/// 设备高度,跟横竖屏无关 +#define DEVICE_HEIGHT MAX([[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height) + +/// 在 iPad 分屏模式下等于 app 实际运行宽度,否则等同于 SCREEN_WIDTH +#define APPLICATION_WIDTH [QMUIHelper applicationSize].width + +/// 在 iPad 分屏模式下等于 app 实际运行宽度,否则等同于 DEVICE_HEIGHT +#define APPLICATION_HEIGHT [QMUIHelper applicationSize].height /// 是否全面屏设备 #define IS_NOTCHED_SCREEN [QMUIHelper isNotchedScreen] +/// iPhone 14 Pro Max +#define IS_67INCH_SCREEN_AND_IPHONE14 [QMUIHelper is67InchScreenAndiPhone14Later] +/// iPhone 12 Pro Max +#define IS_67INCH_SCREEN [QMUIHelper is67InchScreen] /// iPhone XS Max #define IS_65INCH_SCREEN [QMUIHelper is65InchScreen] +/// iPhone 14 Pro / 15 Pro +#define IS_61INCH_SCREEN_AND_IPHONE14PRO [QMUIHelper is61InchScreenAndiPhone14ProLater] +/// iPhone 12 / 12 Pro +#define IS_61INCH_SCREEN_AND_IPHONE12 [QMUIHelper is61InchScreenAndiPhone12Later] /// iPhone XR #define IS_61INCH_SCREEN [QMUIHelper is61InchScreen] /// iPhone X/XS #define IS_58INCH_SCREEN [QMUIHelper is58InchScreen] /// iPhone 6/7/8 Plus #define IS_55INCH_SCREEN [QMUIHelper is55InchScreen] +/// iPhone 12 mini +#define IS_54INCH_SCREEN [QMUIHelper is54InchScreen] /// iPhone 6/7/8 #define IS_47INCH_SCREEN [QMUIHelper is47InchScreen] /// iPhone 5/5S/SE @@ -123,6 +179,9 @@ /// 是否放大模式(iPhone 6及以上的设备支持放大模式,iPhone X 除外) #define IS_ZOOMEDMODE [QMUIHelper isZoomedMode] +/// 当前设备是否拥有灵动岛 +#define IS_DYNAMICISLAND_DEVICE [QMUIHelper isDynamicIslandDevice] + #pragma mark - 变量-布局相关 /// 获取一个像素 @@ -135,30 +194,46 @@ #define ScreenNativeScale ([[UIScreen mainScreen] nativeScale]) /// toolBar相关frame -#define ToolBarHeight (IS_IPAD ? (IS_NOTCHED_SCREEN ? 65 : 50) : (IS_LANDSCAPE ? PreferredValueForVisualDevice(44, 32) : 44) + PreferredValueForNotchedDevice(39, 0)) +#define ToolBarHeight (IS_IPAD ? (IS_NOTCHED_SCREEN ? 70 : 50) : (IS_LANDSCAPE ? PreferredValueForVisualDevice(44, 32) : 44) + SafeAreaInsetsConstantForDeviceWithNotch.bottom) /// tabBar相关frame -#define TabBarHeight (IS_IPAD ? (IS_NOTCHED_SCREEN ? 65 : 50) : PreferredValueForNotchedDevice(IS_LANDSCAPE ? 32 : 49, 49) + SafeAreaInsetsConstantForDeviceWithNotch.bottom) +#define TabBarHeight (IS_IPAD ? (IS_NOTCHED_SCREEN ? 65 : 50) : (IS_LANDSCAPE ? PreferredValueForVisualDevice(49, 32) : 49) + SafeAreaInsetsConstantForDeviceWithNotch.bottom) -/// 状态栏高度(来电等情况下,状态栏高度会发生变化,所以应该实时计算) -#define StatusBarHeight ([UIApplication sharedApplication].statusBarHidden ? 0 : [[UIApplication sharedApplication] statusBarFrame].size.height) +/// 状态栏高度(来电等情况下,状态栏高度会发生变化,所以应该实时计算,iOS 13 起,来电等情况下状态栏高度不会改变) +#define StatusBarHeight (UIApplication.sharedApplication.statusBarHidden ? 0 : UIApplication.sharedApplication.statusBarFrame.size.height) /// 状态栏高度(如果状态栏不可见,也会返回一个普通状态下可见的高度) -#define StatusBarHeightConstant ([UIApplication sharedApplication].statusBarHidden ? (IS_IPAD ? (IS_NOTCHED_SCREEN ? 24 : 20) : PreferredValueForNotchedDevice(IS_LANDSCAPE ? 0 : 44, 20)) : [[UIApplication sharedApplication] statusBarFrame].size.height) +#define StatusBarHeightConstant [QMUIHelper statusBarHeightConstant] /// navigationBar 的静态高度 -#define NavigationBarHeight (IS_LANDSCAPE ? PreferredValueForVisualDevice(44, 32) : 44) +#define NavigationBarHeight (IS_IPAD ? 50 : (IS_LANDSCAPE ? PreferredValueForVisualDevice(44, 32) : 44)) /// 代表(导航栏+状态栏),这里用于获取其高度 /// @warn 如果是用于 viewController,请使用 UIViewController(QMUI) qmui_navigationBarMaxYInViewCoordinator 代替 #define NavigationContentTop (StatusBarHeight + NavigationBarHeight) /// 同上,这里用于获取它的静态常量值 -#define NavigationContentTopConstant (StatusBarHeightConstant + NavigationBarHeight) +#define NavigationContentTopConstant (QMUIHelper.navigationBarMaxYConstant) + +/// 判断当前是否是处于分屏模式的 iPad 或 iOS 16.1 的台前调度模式 +#define IS_SPLIT_SCREEN_IPAD (IS_IPAD && APPLICATION_WIDTH != SCREEN_WIDTH) /// iPhoneX 系列全面屏手机的安全区域的静态值 #define SafeAreaInsetsConstantForDeviceWithNotch [QMUIHelper safeAreaInsetsForDeviceWithNotch] +/// 将所有屏幕按照宽松/紧凑分类,其中 iPad、iPhone XS Max/XR/Plus 均为宽松屏幕,但开启了放大模式的设备均会视为紧凑屏幕 +#define PreferredValueForVisualDevice(_regular, _compact) ([QMUIHelper isRegularScreen] ? _regular : _compact) + +/// 将所有屏幕按照 Phone/Pad 分类,由于历史上宽高比最大(最胖)的手机为 iPhone 4,所以这里以它为基准,只要宽高比比 iPhone 4 更小的,都视为 Phone,其他情况均视为 Pad。注意 iPad 分屏则取分屏后的宽高来计算。 +#define PreferredValueForInterfaceIdiom(_phone, _pad) (APPLICATION_WIDTH / APPLICATION_HEIGHT <= QMUIHelper.screenSizeFor35Inch.width / QMUIHelper.screenSizeFor35Inch.height ? _phone : _pad) + +/// 区分全面屏和非全面屏 +#define PreferredValueForNotchedDevice(_notchedDevice, _otherDevice) ([QMUIHelper isNotchedScreen] ? _notchedDevice : _otherDevice) + + +#pragma mark - 变量-布局相关-已废弃 +/// 由于 iOS 设备屏幕碎片化越来越严重,因此以下这些宏不建议使用,以后有设备更新也不再维护,请使用 PreferredValueForVisualDevice、PreferredValueForInterfaceIdiom 代替。 + /// 按屏幕宽度来区分不同 iPhone 尺寸,iPhone XS Max/XR/Plus 归为一类,iPhone X/8/7/6 归为一类。 /// iPad 也会视为最大的屏幕宽度来处理 #define PreferredValueForiPhone(_65or61or55inch, _47or58inch, _40inch, _35inch) PreferredValueForDeviceIncludingiPad(_65or61or55inch, _65or61or55inch, _47or58inch, _40inch, _35inch) @@ -166,14 +241,31 @@ /// 同上,单独将 iPad 区分对待 #define PreferredValueForDeviceIncludingiPad(_iPad, _65or61or55inch, _47or58inch, _40inch, _35inch) PreferredValueForAll(_iPad, _65or61or55inch, _65or61or55inch, _47or58inch, _65or61or55inch, _47or58inch, _40inch, _35inch) -/// 区分全面屏(iPhone X 系列)和非全面屏 -#define PreferredValueForNotchedDevice(_notchedDevice, _otherDevice) ([QMUIHelper isNotchedScreen] ? _notchedDevice : _otherDevice) - -/// 将所有屏幕按照宽松/紧凑分类,其中 iPad、iPhone XS Max/XR/Plus 均为宽松屏幕,但开启了放大模式的设备均会视为紧凑屏幕 -#define PreferredValueForVisualDevice(_regular, _compact) ([QMUIHelper isRegularScreen] ? _regular : _compact) - -/// 区分全部的设备类型 -#define PreferredValueForAll(_iPad, _65inch, _61inch, _58inch, _55inch, _47inch, _40inch, _35inch) (IS_IPAD ? _iPad : (IS_35INCH_SCREEN ? _35inch : (IS_40INCH_SCREEN ? _40inch : ((IS_47INCH_SCREEN || (IS_55INCH_SCREEN && IS_ZOOMEDMODE)) ? _47inch : (IS_55INCH_SCREEN ? _55inch : ((IS_58INCH_SCREEN || ((IS_61INCH_SCREEN || IS_65INCH_SCREEN) && IS_ZOOMEDMODE)) ? _58inch : (IS_61INCH_SCREEN ? _61inch : _65inch))))))) +/// 若 iPad 处于分屏模式下,返回 iPad 接近 iPhone 宽度(320、375、414)中近似的一种,方便屏幕适配。 +#define IPAD_SIMILAR_SCREEN_WIDTH [QMUIHelper preferredLayoutAsSimilarScreenWidthForIPad] + +#define _40INCH_WIDTH [QMUIHelper screenSizeFor40Inch].width +#define _58INCH_WIDTH [QMUIHelper screenSizeFor58Inch].width +#define _65INCH_WIDTH [QMUIHelper screenSizeFor65Inch].width + +#define AS_IPAD (DynamicPreferredValueForIPad ? ((IS_IPAD && !IS_SPLIT_SCREEN_IPAD) || (IS_SPLIT_SCREEN_IPAD && APPLICATION_WIDTH >= 768)) : IS_IPAD) +#define AS_65INCH_SCREEN (IS_67INCH_SCREEN_AND_IPHONE14 || IS_67INCH_SCREEN || IS_65INCH_SCREEN || (IS_IPAD && DynamicPreferredValueForIPad && IPAD_SIMILAR_SCREEN_WIDTH == _65INCH_WIDTH)) +#define AS_61INCH_SCREEN (IS_61INCH_SCREEN_AND_IPHONE12 || IS_61INCH_SCREEN) +#define AS_58INCH_SCREEN (IS_58INCH_SCREEN || IS_54INCH_SCREEN || ((AS_61INCH_SCREEN || AS_65INCH_SCREEN) && IS_ZOOMEDMODE) || (IS_IPAD && DynamicPreferredValueForIPad && IPAD_SIMILAR_SCREEN_WIDTH == _58INCH_WIDTH)) +#define AS_55INCH_SCREEN (IS_55INCH_SCREEN) +#define AS_47INCH_SCREEN (IS_47INCH_SCREEN || (IS_55INCH_SCREEN && IS_ZOOMEDMODE)) +#define AS_40INCH_SCREEN (IS_40INCH_SCREEN || (IS_IPAD && DynamicPreferredValueForIPad && IPAD_SIMILAR_SCREEN_WIDTH == _40INCH_WIDTH)) +#define AS_35INCH_SCREEN IS_35INCH_SCREEN +#define AS_320WIDTH_SCREEN IS_320WIDTH_SCREEN + +#define PreferredValueForAll(_iPad, _65inch, _61inch, _58inch, _55inch, _47inch, _40inch, _35inch) \ +(AS_IPAD ? _iPad :\ +(AS_35INCH_SCREEN ? _35inch :\ +(AS_40INCH_SCREEN ? _40inch :\ +(AS_47INCH_SCREEN ? _47inch :\ +(AS_55INCH_SCREEN ? _55inch :\ +(AS_58INCH_SCREEN ? _58inch :\ +(AS_61INCH_SCREEN ? _61inch : _65inch))))))) #pragma mark - 方法-创建器 @@ -222,15 +314,39 @@ AddAccessibilityHint(NSObject *obj, NSString *hint) { #define StringFromBOOL(_flag) (_flag ? @"YES" : @"NO") +/// 代替 NSAssert 使用,在触发 assert 之前会用 QMUILogWarn 输出日志,当你开启了配置表的 ShouldPrintQMUIWarnLogToConsole 时,会用 QMUIConsole 代替 NSAssert,避免中断当前程序的运行 +/// 与 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__);}}}) + +#pragma mark - Selector + +/** + 根据给定的 getter selector 获取对应的 setter selector + @param getter 目标 getter selector + @return 对应的 setter selector + */ +CG_INLINE SEL +setterWithGetter(SEL getter) { + NSString *getterString = NSStringFromSelector(getter); + NSMutableString *setterString = [[NSMutableString alloc] initWithString:@"set"]; + [setterString appendString:getterString.qmui_capitalizedString]; + [setterString appendString:@":"]; + SEL setter = NSSelectorFromString(setterString); + return setter; +} + #pragma mark - CGFloat /** * 某些地方可能会将 CGFLOAT_MIN 作为一个数值参与计算(但其实 CGFLOAT_MIN 更应该被视为一个标志位而不是数值),可能导致一些精度问题,所以提供这个方法快速将 CGFLOAT_MIN 转换为 0 + * 某些情况可能计算出来是0.0000000x,也靠这个方法抹去尾数。 * issue: https://github.com/Tencent/QMUI_iOS/issues/203 */ CG_INLINE CGFloat removeFloatMin(CGFloat floatValue) { - return floatValue == CGFLOAT_MIN ? 0 : floatValue; + return fabs(floatValue) <= 0.001 ? 0 : floatValue; } /** @@ -240,9 +356,19 @@ removeFloatMin(CGFloat floatValue) { */ CG_INLINE CGFloat flatSpecificScale(CGFloat floatValue, CGFloat scale) { + if (isinf(floatValue) || floatValue == CGFLOAT_MAX) return floatValue; floatValue = removeFloatMin(floatValue); scale = scale ?: ScreenScale; - CGFloat flattedValue = ceil(floatValue * scale) / scale; + // 这里因为浮点精度的问题,可能会出现一些偏差,例如 161.66666666666669 算出来可能是162,161.66666666666666 算出来是161.66666666667,为了解决这种场景,这里同时用 ceil 和 round 算一遍再取最接近的那个结果 + NSInteger pixelValue1 = ceil(floatValue * scale); + NSInteger pixelValue2 = round(floatValue * scale); + NSInteger pixelValue = 0; + if (fabs(pixelValue1 - floatValue) <= fabs(pixelValue2 - floatValue)) { + pixelValue = pixelValue1; + } else { + pixelValue = pixelValue2; + } + CGFloat flattedValue = pixelValue / scale; return flattedValue; } @@ -282,7 +408,7 @@ betweenOrEqual(CGFloat minimumValue, CGFloat value, CGFloat maximumValue) { * 例如 CGFloatToFixed(0.3333, 2) 会返回 0.33,而 CGFloatToFixed(0.6666, 2) 会返回 0.67 * * @warning 参数类型为 CGFloat,也即意味着不管传进来的是 float 还是 double 最终都会被强制转换成 CGFloat 再做计算 - * @warning 该方法无法解决浮点数精度运算的问题 + * @warning 该方法无法解决浮点数精度运算的问题,如需做浮点数的 == 判断,可用下方的 CGFloatEqualToFloat() */ CG_INLINE CGFloat CGFloatToFixed(CGFloat value, NSUInteger precision) { @@ -296,6 +422,58 @@ CGFloatToFixed(CGFloat value, NSUInteger precision) { return result; } +/** + 用于两个 CGFloat 值之间的比较运算,支持 ==、>、<、>=、<= 5种,内部会将浮点数转成整型,从而避免浮点数精度导致的判断错误。 + + CGFloatEqualToFloatWithPrecision() + CGFloatEqualToFloat() + CGFloatMoreThanFloatWithPrecision() + CGFloatMoreThanFloat() + CGFloatMoreThanOrEqualToFloatWithPrecision() + CGFloatMoreThanOrEqualToFloat() + CGFloatLessThanFloatWithPrecision() + CGFloatLessThanFloat() + CGFloatLessThanOrEqualToFloatWithPrecision() + CGFloatLessThanOrEqualToFloat() + + 可通过参数 precision 指定要考虑的小数点后的精度,精度的定义是保证指定的那一位小数点不会因为浮点问题导致计算错误,例如当我们要获取一个 1.0 的浮点数时,有时候会得到 0.99999999,有时候会得到 1.000000001,所以需要对指定的那一位小数点的后一位数进行四舍五入操作。 + @code + precision = 0,也即对小数点后0+1位四舍五入 + 0.999 -> 0.9 -> round(0.9) -> 1 + 1.011 -> 1.0 -> round(1.0) -> 1 + 1.033 -> 1.0 -> round(1.0) -> 1 + 1.099 -> 1.0 -> round(1.0) -> 1 + precision = 1,也即对小数点后1+1位四舍五入 + 0.999 -> 9.9 -> round(9.9) -> 10 -> 1.0 + 1.011 -> 10.1 -> round(10.1) -> 10 -> 1.0 + 1.033 -> 10.3 -> round(10.3) -> 10 -> 1.0 + 1.099 -> 10.9 -> round(10.9) -> 11 -> 1.1 + precision = 2,也即对小数点后2+1位四舍五入 + 0.999 -> 99.9 -> round(99.9) -> 100 -> 1.00 + 1.011 -> 101.1 -> round(101.1) -> 101 -> 1.01 + 1.033 -> 103.3 -> round(103.3) -> 103 -> 1.03 + 1.099 -> 109.9 -> round(109.9) -> 110 -> 1.1 + @endcode +*/ +CG_INLINE NSInteger _RoundedIntegerFromCGFloat(CGFloat value, NSUInteger precision) { + return (NSInteger)(round(value * pow(10, precision))); +} + +#define _CGFloatCalcGenerator(_operatorName, _operator) CG_INLINE BOOL CGFloat##_operatorName##FloatWithPrecision(CGFloat value1, CGFloat value2, NSUInteger precision) {\ + NSInteger a = _RoundedIntegerFromCGFloat(value1, precision);\ + NSInteger b = _RoundedIntegerFromCGFloat(value2, precision);\ + return a _operator b;\ +}\ +CG_INLINE BOOL CGFloat##_operatorName##Float(CGFloat value1, CGFloat value2) {\ + return CGFloat##_operatorName##FloatWithPrecision(value1, value2, 0);\ +} + +_CGFloatCalcGenerator(EqualTo, ==) +_CGFloatCalcGenerator(LessThan, <) +_CGFloatCalcGenerator(LessThanOrEqualTo, <=) +_CGFloatCalcGenerator(MoreThan, >) +_CGFloatCalcGenerator(MoreThanOrEqualTo, >=) + /// 用于居中运算 CG_INLINE CGFloat CGFloatGetCenter(CGFloat parent, CGFloat child) { @@ -553,14 +731,10 @@ CGRectGetMinXHorizontallyCenter(CGRect referenceRect, CGRect layoutingRect) { return CGRectGetMinX(referenceRect) + CGRectGetMinXHorizontallyCenterInParentRect(referenceRect, layoutingRect); } -/// 为给定的rect往内部缩小insets的大小 +/// 为给定的rect往内部缩小insets的大小(系统那个方法的命名太难联想了,所以定义了一个新函数) CG_INLINE CGRect CGRectInsetEdges(CGRect rect, UIEdgeInsets insets) { - rect.origin.x += insets.left; - rect.origin.y += insets.top; - rect.size.width -= UIEdgeInsetsGetHorizontalValue(insets); - rect.size.height -= UIEdgeInsetsGetVerticalValue(insets); - return rect; + return UIEdgeInsetsInsetRect(rect, insets); } /// 传入size,返回一个x/y为0的CGRect diff --git a/QMUI/QMUIKit/QMUICore/QMUIConfiguration.h b/QMUI/QMUIKit/QMUICore/QMUIConfiguration.h index 7d54586b..872cee9a 100644 --- a/QMUI/QMUIKit/QMUICore/QMUIConfiguration.h +++ b/QMUI/QMUIKit/QMUICore/QMUIConfiguration.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIConfiguration.h @@ -16,6 +16,8 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + /// 所有配置表都应该实现的 protocol /// All configuration templates should implement this protocal @protocol QMUIConfigurationTemplateProtocol @@ -26,7 +28,7 @@ - (void)applyConfigurationTemplate; @optional -/// 当返回 YES 时,启动 App 的时候 QMUIConfiguration 会自动应用这份配置表。但启动 App 时自动应用的配置表最多只允许一份,如果有多份则其他的会被忽略,需要在某些时机手动应用 +/// 当返回 YES 时,启动 App 的时候 QMUIConfiguration 会自动应用这份配置表。但启动 App 时自动应用的配置表最多只允许一份,如果有多份则其他的会被忽略 /// QMUIConfiguration automatically applies this template on launch when set to YES. Since only one copy of configuration template is allowed when the app launches, you'll have to call `applyConfigurationTemplate` manually if you have more than one configuration templates. - (BOOL)shouldApplyTemplateAutomatically; @@ -38,8 +40,6 @@ */ @interface QMUIConfiguration : NSObject -NS_ASSUME_NONNULL_BEGIN - /// 标志当前项目是否有使用配置表功能 @property(nonatomic, assign, readonly) BOOL active; @@ -79,31 +79,33 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, assign) CGFloat buttonHighlightedAlpha; @property(nonatomic, assign) CGFloat buttonDisabledAlpha; @property(nonatomic, strong, nullable) UIColor *buttonTintColor; -@property(nonatomic, strong) UIColor *ghostButtonColorBlue; -@property(nonatomic, strong) UIColor *ghostButtonColorRed; -@property(nonatomic, strong) UIColor *ghostButtonColorGreen; -@property(nonatomic, strong) UIColor *ghostButtonColorGray; -@property(nonatomic, strong) UIColor *ghostButtonColorWhite; -@property(nonatomic, strong) UIColor *fillButtonColorBlue; -@property(nonatomic, strong) UIColor *fillButtonColorRed; -@property(nonatomic, strong) UIColor *fillButtonColorGreen; -@property(nonatomic, strong) UIColor *fillButtonColorGray; -@property(nonatomic, strong) UIColor *fillButtonColorWhite; #pragma mark - UITextField & UITextView +@property(nonatomic, strong, nullable) UIColor *textFieldTextColor; @property(nonatomic, strong, nullable) UIColor *textFieldTintColor; @property(nonatomic, assign) UIEdgeInsets textFieldTextInsets; +@property(nonatomic, assign) UIKeyboardAppearance keyboardAppearance; + +#pragma mark - UISwitch +@property(nonatomic, strong, nullable) UIColor *switchOnTintColor; +@property(nonatomic, strong, nullable) UIColor *switchOffTintColor; +@property(nonatomic, strong, nullable) UIColor *switchThumbTintColor; #pragma mark - NavigationBar +@property(nonatomic, assign) BOOL navBarUsesStandardAppearanceOnly API_AVAILABLE(ios(15.0)); +@property(nonatomic, copy, nullable) NSArray> *navBarContainerClasses; @property(nonatomic, assign) CGFloat navBarHighlightedAlpha; @property(nonatomic, assign) CGFloat navBarDisabledAlpha; @property(nonatomic, strong, nullable) UIFont *navBarButtonFont; @property(nonatomic, strong, nullable) UIFont *navBarButtonFontBold; @property(nonatomic, strong, nullable) UIImage *navBarBackgroundImage; +@property(nonatomic, assign) BOOL navBarRemoveBackgroundEffectAutomatically API_AVAILABLE(ios(15.0)); @property(nonatomic, strong, nullable) UIImage *navBarShadowImage; +@property(nonatomic, strong, nullable) UIColor *navBarShadowImageColor; @property(nonatomic, strong, nullable) UIColor *navBarBarTintColor; +@property(nonatomic, assign) UIBarStyle navBarStyle; @property(nonatomic, strong, nullable) UIColor *navBarTintColor; @property(nonatomic, strong, nullable) UIColor *navBarTitleColor; @property(nonatomic, strong, nullable) UIFont *navBarTitleFont; @@ -121,32 +123,41 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - TabBar +@property(nonatomic, assign) BOOL tabBarUsesStandardAppearanceOnly API_AVAILABLE(ios(15.0)); +@property(nonatomic, copy, nullable) NSArray> *tabBarContainerClasses; @property(nonatomic, strong, nullable) UIImage *tabBarBackgroundImage; +@property(nonatomic, assign) BOOL tabBarRemoveBackgroundEffectAutomatically API_AVAILABLE(ios(15.0)); @property(nonatomic, strong, nullable) UIColor *tabBarBarTintColor; @property(nonatomic, strong, nullable) UIColor *tabBarShadowImageColor; -@property(nonatomic, strong, nullable) UIColor *tabBarTintColor; +@property(nonatomic, assign) UIBarStyle tabBarStyle; +@property(nonatomic, strong, nullable) UIFont *tabBarItemTitleFont; +@property(nonatomic, strong, nullable) UIFont *tabBarItemTitleFontSelected; @property(nonatomic, strong, nullable) UIColor *tabBarItemTitleColor; @property(nonatomic, strong, nullable) UIColor *tabBarItemTitleColorSelected; -@property(nonatomic, strong, nullable) UIFont *tabBarItemTitleFont; +@property(nonatomic, strong, nullable) UIColor *tabBarItemImageColor; +@property(nonatomic, strong, nullable) UIColor *tabBarItemImageColorSelected; #pragma mark - Toolbar +@property(nonatomic, assign) BOOL toolBarUsesStandardAppearanceOnly API_AVAILABLE(ios(15.0)); +@property(nonatomic, copy, nullable) NSArray> *toolBarContainerClasses; @property(nonatomic, assign) CGFloat toolBarHighlightedAlpha; @property(nonatomic, assign) CGFloat toolBarDisabledAlpha; @property(nonatomic, strong, nullable) UIColor *toolBarTintColor; @property(nonatomic, strong, nullable) UIColor *toolBarTintColorHighlighted; @property(nonatomic, strong, nullable) UIColor *toolBarTintColorDisabled; @property(nonatomic, strong, nullable) UIImage *toolBarBackgroundImage; +@property(nonatomic, assign) BOOL toolBarRemoveBackgroundEffectAutomatically API_AVAILABLE(ios(15.0)); @property(nonatomic, strong, nullable) UIColor *toolBarBarTintColor; @property(nonatomic, strong, nullable) UIColor *toolBarShadowImageColor; +@property(nonatomic, assign) UIBarStyle toolBarStyle; @property(nonatomic, strong, nullable) UIFont *toolBarButtonFont; #pragma mark - SearchBar -@property(nonatomic, strong, nullable) UIColor *searchBarTextFieldBackground; +@property(nonatomic, strong, nullable) UIImage *searchBarTextFieldBackgroundImage; @property(nonatomic, strong, nullable) UIColor *searchBarTextFieldBorderColor; -@property(nonatomic, strong, nullable) UIColor *searchBarBottomBorderColor; -@property(nonatomic, strong, nullable) UIColor *searchBarBarTintColor; +@property(nonatomic, strong, nullable) UIImage *searchBarBackgroundImage; @property(nonatomic, strong, nullable) UIColor *searchBarTintColor; @property(nonatomic, strong, nullable) UIColor *searchBarTextColor; @property(nonatomic, strong, nullable) UIColor *searchBarPlaceholderColor; @@ -162,7 +173,6 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, assign) BOOL tableViewEstimatedHeightEnabled; @property(nonatomic, strong, nullable) UIColor *tableViewBackgroundColor; -@property(nonatomic, strong, nullable) UIColor *tableViewGroupedBackgroundColor; @property(nonatomic, strong, nullable) UIColor *tableSectionIndexColor; @property(nonatomic, strong, nullable) UIColor *tableSectionIndexBackgroundColor; @property(nonatomic, strong, nullable) UIColor *tableSectionIndexTrackingBackgroundColor; @@ -189,7 +199,15 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, assign) UIEdgeInsets tableViewSectionFooterAccessoryMargins; @property(nonatomic, assign) UIEdgeInsets tableViewSectionHeaderContentInset; @property(nonatomic, assign) UIEdgeInsets tableViewSectionFooterContentInset; +@property(nonatomic, assign) CGFloat tableViewSectionHeaderTopPadding API_AVAILABLE(ios(15.0)); +@property(nonatomic, strong, nullable) UIColor *tableViewGroupedBackgroundColor; +@property(nonatomic, strong, nullable) UIColor *tableViewGroupedSeparatorColor; +@property(nonatomic, strong, nullable) UIColor *tableViewGroupedCellTitleLabelColor; +@property(nonatomic, strong, nullable) UIColor *tableViewGroupedCellDetailLabelColor; +@property(nonatomic, strong, nullable) UIColor *tableViewGroupedCellBackgroundColor; +@property(nonatomic, strong, nullable) UIColor *tableViewGroupedCellSelectedBackgroundColor; +@property(nonatomic, strong, nullable) UIColor *tableViewGroupedCellWarningBackgroundColor; @property(nonatomic, strong, nullable) UIFont *tableViewGroupedSectionHeaderFont; @property(nonatomic, strong, nullable) UIFont *tableViewGroupedSectionFooterFont; @property(nonatomic, strong, nullable) UIColor *tableViewGroupedSectionHeaderTextColor; @@ -200,47 +218,70 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, assign) CGFloat tableViewGroupedSectionFooterDefaultHeight; @property(nonatomic, assign) UIEdgeInsets tableViewGroupedSectionHeaderContentInset; @property(nonatomic, assign) UIEdgeInsets tableViewGroupedSectionFooterContentInset; +@property(nonatomic, assign) CGFloat tableViewGroupedSectionHeaderTopPadding API_AVAILABLE(ios(15.0)); + +@property(nonatomic, assign) CGFloat tableViewInsetGroupedCornerRadius; +@property(nonatomic, assign) CGFloat tableViewInsetGroupedHorizontalInset; +@property(nonatomic, strong, nullable) UIColor *tableViewInsetGroupedBackgroundColor; +@property(nonatomic, strong, nullable) UIColor *tableViewInsetGroupedSeparatorColor; +@property(nonatomic, strong, nullable) UIColor *tableViewInsetGroupedCellTitleLabelColor; +@property(nonatomic, strong, nullable) UIColor *tableViewInsetGroupedCellDetailLabelColor; +@property(nonatomic, strong, nullable) UIColor *tableViewInsetGroupedCellBackgroundColor; +@property(nonatomic, strong, nullable) UIColor *tableViewInsetGroupedCellSelectedBackgroundColor; +@property(nonatomic, strong, nullable) UIColor *tableViewInsetGroupedCellWarningBackgroundColor; +@property(nonatomic, strong, nullable) UIFont *tableViewInsetGroupedSectionHeaderFont; +@property(nonatomic, strong, nullable) UIFont *tableViewInsetGroupedSectionFooterFont; +@property(nonatomic, strong, nullable) UIColor *tableViewInsetGroupedSectionHeaderTextColor; +@property(nonatomic, strong, nullable) UIColor *tableViewInsetGroupedSectionFooterTextColor; +@property(nonatomic, assign) UIEdgeInsets tableViewInsetGroupedSectionHeaderAccessoryMargins; +@property(nonatomic, assign) UIEdgeInsets tableViewInsetGroupedSectionFooterAccessoryMargins; +@property(nonatomic, assign) CGFloat tableViewInsetGroupedSectionHeaderDefaultHeight; +@property(nonatomic, assign) CGFloat tableViewInsetGroupedSectionFooterDefaultHeight; +@property(nonatomic, assign) UIEdgeInsets tableViewInsetGroupedSectionHeaderContentInset; +@property(nonatomic, assign) UIEdgeInsets tableViewInsetGroupedSectionFooterContentInset; +@property(nonatomic, assign) CGFloat tableViewInsetGroupedSectionHeaderTopPadding API_AVAILABLE(ios(15.0)); #pragma mark - UIWindowLevel @property(nonatomic, assign) CGFloat windowLevelQMUIAlertView; +@property(nonatomic, assign) CGFloat windowLevelQMUIConsole; #pragma mark - QMUILog @property(nonatomic, assign) BOOL shouldPrintDefaultLog; @property(nonatomic, assign) BOOL shouldPrintInfoLog; @property(nonatomic, assign) BOOL shouldPrintWarnLog; +@property(nonatomic, assign) BOOL shouldPrintQMUIWarnLogToConsole; #pragma mark - QMUIBadge -@property(nonatomic, strong, nullable) UIColor *badgeBackgroundColor; -@property(nonatomic, strong, nullable) UIColor *badgeTextColor; -@property(nonatomic, strong, nullable) UIFont *badgeFont; -@property(nonatomic, assign) UIEdgeInsets badgeContentEdgeInsets; -@property(nonatomic, assign) CGPoint badgeCenterOffset; -@property(nonatomic, assign) CGPoint badgeCenterOffsetLandscape; +@property(nonatomic, strong, nullable) UIColor *badgeBackgroundColor; +@property(nonatomic, strong, nullable) UIColor *badgeTextColor; +@property(nonatomic, strong, nullable) UIFont *badgeFont; +@property(nonatomic, assign) UIEdgeInsets badgeContentEdgeInsets; +@property(nonatomic, assign) CGPoint badgeOffset; +@property(nonatomic, assign) CGPoint badgeOffsetLandscape; -@property(nonatomic, strong, nullable) UIColor *updatesIndicatorColor; -@property(nonatomic, assign) CGSize updatesIndicatorSize; -@property(nonatomic, assign) CGPoint updatesIndicatorCenterOffset; -@property(nonatomic, assign) CGPoint updatesIndicatorCenterOffsetLandscape; +@property(nonatomic, strong, nullable) UIColor *updatesIndicatorColor; +@property(nonatomic, assign) CGSize updatesIndicatorSize; +@property(nonatomic, assign) CGPoint updatesIndicatorOffset; +@property(nonatomic, assign) CGPoint updatesIndicatorOffsetLandscape; #pragma mark - Others @property(nonatomic, assign) BOOL automaticCustomNavigationBarTransitionStyle; @property(nonatomic, assign) UIInterfaceOrientationMask supportedOrientationMask; @property(nonatomic, assign) BOOL automaticallyRotateDeviceOrientation; -@property(nonatomic, assign) BOOL statusbarStyleLightInitially; +@property(nonatomic, assign) UIStatusBarStyle defaultStatusBarStyle; @property(nonatomic, assign) BOOL needsBackBarButtonItemTitle; @property(nonatomic, assign) BOOL hidesBottomBarWhenPushedInitially; @property(nonatomic, assign) BOOL preventConcurrentNavigationControllerTransitions; @property(nonatomic, assign) BOOL navigationBarHiddenInitially; -@property(nonatomic, assign) BOOL shouldFixTabBarTransitionBugInIPhoneX; -@property(nonatomic, assign) BOOL shouldFixTabBarButtonBugForAll; -@property(nonatomic, assign) BOOL shouldPrintQMUIWarnLogToConsole; -@property(nonatomic, assign) BOOL sendAnalyticsToQMUITeam; - -NS_ASSUME_NONNULL_END +@property(nonatomic, assign) BOOL shouldFixTabBarSafeAreaInsetsBug; +@property(nonatomic, assign) BOOL shouldFixSearchBarMaskViewLayoutBug; +@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)); /// 单例对象 /// The singleton instance @@ -248,3 +289,34 @@ NS_ASSUME_NONNULL_END - (void)applyInitialTemplate; @end + +@interface UINavigationBar (QMUIConfiguration) + +/** + 返回由配置表项 NavBarContainerClasses 配置的 UINavigationBar appearance 对象,用于代替 [UINavigationBar appearanceWhenContainedInInstancesOfClasses:NavBarContainerClasses] 的冗长写法。当配置表项 NavBarContainerClasses 为 nil 或空数组时,本方法等价于 UINavigationBar.appearance。 + */ ++ (instancetype)qmui_appearanceConfigured; +@end + +@interface UITabBar (QMUIConfiguration) + +/** + 返回由配置表项 TabBarContainerClasses 配置的 UITabBar appearance 对象,用于代替 [UITabBar appearanceWhenContainedInInstancesOfClasses:TabBarContainerClasses] 的冗长写法。当配置表项 TabBarContainerClasses 为 nil 或空数组时,本方法等价于 UITabBar.appearance。 + */ ++ (instancetype)qmui_appearanceConfigured; +@end + +@interface UIToolbar (QMUIConfiguration) + +/** + 返回由配置表项 ToolBarContainerClasses 配置的 UIToolbar appearance 对象,用于代替 [UIToolbar appearanceWhenContainedInInstancesOfClasses:ToolBarContainerClasses] 的冗长写法。当配置表项 ToolBarContainerClasses 为 nil 或空数组时,本方法等价于 UIToolbar.appearance。 + */ ++ (instancetype)qmui_appearanceConfigured; +@end + +@interface UITabBarItem (QMUIConfiguration) + ++ (instancetype)qmui_appearanceConfigured; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUICore/QMUIConfiguration.m b/QMUI/QMUIKit/QMUICore/QMUIConfiguration.m index baa4c96f..ef74deb8 100644 --- a/QMUI/QMUIKit/QMUICore/QMUIConfiguration.m +++ b/QMUI/QMUIKit/QMUICore/QMUIConfiguration.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIConfiguration.m @@ -18,13 +18,48 @@ #import "UIImage+QMUI.h" #import "NSString+QMUI.h" #import "UIViewController+QMUI.h" -#import "QMUIKit.h" -#import +#import "QMUIKit.h"// 为了引入其中定义的 QMUI_VERSION // 在 iOS 8 - 11 上实际测量得到 // Measured on iOS 8 - 11 const CGSize kUINavigationBarBackIndicatorImageSize = {13, 21}; +@interface QMUIConfiguration () + +@property(nonatomic, strong) UINavigationBarAppearance *navigationBarAppearance API_AVAILABLE(ios(15.0)); +@property(nonatomic, strong) UIToolbarAppearance *toolBarAppearance API_AVAILABLE(ios(15.0)); +@property(nonatomic, strong) UITabBarAppearance *tabBarAppearance API_AVAILABLE(ios(13.0)); +@end + +@implementation UIViewController (QMUIConfiguration) + +- (NSArray *)qmui_existingViewControllersOfClasses:(NSArray> *)classes { + NSMutableSet *viewControllers = [NSMutableSet set]; + if (self.presentedViewController) { + [viewControllers addObjectsFromArray:[self.presentedViewController qmui_existingViewControllersOfClasses:classes]]; + } + if ([self isKindOfClass:UINavigationController.class]) { + [viewControllers addObjectsFromArray:[((UINavigationController *)self).visibleViewController qmui_existingViewControllersOfClasses:classes]]; + } else if ([self isKindOfClass:UITabBarController.class]) { + [viewControllers addObjectsFromArray:[((UITabBarController *)self).selectedViewController qmui_existingViewControllersOfClasses:classes]]; + } else { + // 如果不是常见的 container viewController,则直接获取所有 childViewController + for (UIViewController *child in self.childViewControllers) { + [viewControllers addObjectsFromArray:[child qmui_existingViewControllersOfClasses:classes]]; + } + } + + for (Class class in classes) { + if ([self isKindOfClass:class]) { + [viewControllers addObject:self]; + break; + } + } + return viewControllers.allObjects; +} + +@end + @implementation QMUIConfiguration + (instancetype)sharedInstance { @@ -46,7 +81,9 @@ - (instancetype)init { static BOOL QMUI_hasAppliedInitialTemplate; - (void)applyInitialTemplate { - if (QMUI_hasAppliedInitialTemplate) { + // XCTest 无法加载配置表,因此没有寻找 classes 的必要 + // https://github.com/Tencent/QMUI_iOS/issues/1312 + if (QMUI_hasAppliedInitialTemplate || IS_XCTEST) { return; } @@ -54,61 +91,48 @@ - (void)applyInitialTemplate { // Automatically look for templates and apply them // @see https://github.com/Tencent/QMUI_iOS/issues/264 Protocol *protocol = @protocol(QMUIConfigurationTemplateProtocol); - int numberOfClasses = objc_getClassList(NULL, 0); - if (numberOfClasses > 0) { - Class *classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numberOfClasses); - numberOfClasses = objc_getClassList(classes, numberOfClasses); - for (int i = 0; i < numberOfClasses; i++) { - Class class = classes[i]; - // 这里用 containsString 是考虑到 Swift 里 className 由“项目前缀+class 名”组成,如果用 hasPrefix 就无法判断了 - // Use `containsString` instead of `hasPrefix` because class names in Swift have project prefix prepended - if ([NSStringFromClass(class) containsString:@"QMUIConfigurationTemplate"] && [class conformsToProtocol:protocol]) { - if ([class instancesRespondToSelector:@selector(shouldApplyTemplateAutomatically)]) { - id template = [[class alloc] init]; - if ([template shouldApplyTemplateAutomatically]) { - QMUI_hasAppliedInitialTemplate = YES; - [template applyConfigurationTemplate]; - _active = YES;// 标志配置表已生效 - // 只应用第一个 shouldApplyTemplateAutomatically 的主题 - // Only apply the first template returned - break; - } + classref_t *classesref = nil; + Class *classes = nil; + int numberOfClasses = qmui_getProjectClassList(&classesref); + if (numberOfClasses <= 0) { + numberOfClasses = objc_getClassList(NULL, 0); + classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numberOfClasses); + objc_getClassList(classes, numberOfClasses); + NSAssert(NO, @"如果你看到这条提示,建议到 GitHub 上提 issue,让我们联系你查看项目的配置表使用情况,否则请注释掉这一行。"); + } + for (NSInteger i = 0; i < numberOfClasses; i++) { + Class class = classesref ? (__bridge Class)classesref[i] : classes[i]; + // 这里用 containsString 是考虑到 Swift 里 className 由“项目前缀+class 名”组成,如果用 hasPrefix 就无法判断了 + // Use `containsString` instead of `hasPrefix` because class names in Swift have project prefix prepended + if ([NSStringFromClass(class) containsString:@"QMUIConfigurationTemplate"] && [class conformsToProtocol:protocol]) { + if ([class instancesRespondToSelector:@selector(shouldApplyTemplateAutomatically)]) { + id template = [[class alloc] init]; + if ([template shouldApplyTemplateAutomatically]) { + QMUI_hasAppliedInitialTemplate = YES; + _active = YES;// 标志配置表已生效 + [template applyConfigurationTemplate]; + // 只应用第一个 shouldApplyTemplateAutomatically 的主题 + // Only apply the first template returned + break; } } } - free(classes); } - if (IS_DEBUG && self.sendAnalyticsToQMUITeam) { - [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:[NSOperationQueue new] usingBlock:^(NSNotification * _Nonnull note) { - [self sendAnalytics]; - }]; - } + if (classes) free(classes); QMUI_hasAppliedInitialTemplate = YES; } -- (void)sendAnalytics { - 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 *appInfo = [NSString stringWithFormat:@"appId=%@&appName=%@&version=%@&platform=iOS", identifier, displayName, QMUIVersion]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://qmuiteam.com/analytics/usageReport"]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [appInfo dataUsingEncoding:NSUTF8StringEncoding]; - NSURLSession *session = [NSURLSession sharedSession]; - [[session dataTaskWithRequest:request] resume]; -} - #pragma mark - Initialize default values - (void)initDefaultConfiguration { #pragma mark - Global Color - self.clearColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0]; - self.whiteColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:1]; - self.blackColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:1]; + self.clearColor = UIColorMakeWithRGBA(255, 255, 255, 0); + self.whiteColor = UIColorMake(255, 255, 255); + self.blackColor = UIColorMake(0, 0, 0); self.grayColor = UIColorMake(179, 179, 179); self.grayDarkenColor = UIColorMake(163, 163, 163); self.grayLightenColor = UIColorMake(198, 198, 198); @@ -119,7 +143,6 @@ - (void)initDefaultConfiguration { self.linkColor = UIColorMake(56, 116, 171); self.disabledColor = self.grayColor; - self.backgroundColor = nil; self.maskDarkColor = UIColorMakeWithRGBA(0, 0, 0, .35f); self.maskLightColor = UIColorMakeWithRGBA(255, 255, 255, .5f); self.separatorColor = UIColorMake(222, 224, 226); @@ -141,103 +164,46 @@ - (void)initDefaultConfiguration { self.buttonDisabledAlpha = self.controlDisabledAlpha; self.buttonTintColor = self.blueColor; - self.ghostButtonColorBlue = self.blueColor; - self.ghostButtonColorRed = self.redColor; - self.ghostButtonColorGreen = self.greenColor; - self.ghostButtonColorGray = self.grayColor; - self.ghostButtonColorWhite = self.whiteColor; - - self.fillButtonColorBlue = self.blueColor; - self.fillButtonColorRed = self.redColor; - self.fillButtonColorGreen = self.greenColor; - self.fillButtonColorGray = self.grayColor; - self.fillButtonColorWhite = self.whiteColor; - #pragma mark - UITextField & UITextView - self.textFieldTintColor = nil; self.textFieldTextInsets = UIEdgeInsetsMake(0, 7, 0, 7); #pragma mark - NavigationBar self.navBarHighlightedAlpha = 0.2f; self.navBarDisabledAlpha = 0.2f; - self.navBarButtonFont = nil; - self.navBarButtonFontBold = nil; - self.navBarBackgroundImage = nil; - self.navBarShadowImage = nil; - self.navBarBarTintColor = nil; - self.navBarTintColor = nil; - self.navBarTitleColor = nil; - self.navBarTitleFont = nil; - self.navBarLargeTitleColor = nil; - self.navBarLargeTitleFont = nil; - self.navBarBackButtonTitlePositionAdjustment = UIOffsetZero; self.sizeNavBarBackIndicatorImageAutomatically = YES; - self.navBarBackIndicatorImage = nil; - 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]; - #pragma mark - TabBar + // 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]; + } - self.tabBarBackgroundImage = nil; - self.tabBarBarTintColor = nil; - self.tabBarShadowImageColor = nil; - self.tabBarTintColor = nil; - self.tabBarItemTitleColor = nil; - self.tabBarItemTitleColorSelected = self.tabBarTintColor; - self.tabBarItemTitleFont = nil; #pragma mark - Toolbar self.toolBarHighlightedAlpha = 0.4f; self.toolBarDisabledAlpha = 0.4f; - self.toolBarTintColor = nil; - self.toolBarTintColorHighlighted = [self.toolBarTintColor colorWithAlphaComponent:self.toolBarHighlightedAlpha]; - self.toolBarTintColorDisabled = [self.toolBarTintColor colorWithAlphaComponent:self.toolBarDisabledAlpha]; - self.toolBarBackgroundImage = nil; - self.toolBarBarTintColor = nil; - self.toolBarShadowImageColor = nil; - self.toolBarButtonFont = nil; #pragma mark - SearchBar - self.searchBarTextFieldBackground = nil; - self.searchBarTextFieldBorderColor = nil; - self.searchBarBottomBorderColor = nil; - self.searchBarBarTintColor = nil; - self.searchBarTintColor = nil; - self.searchBarTextColor = nil; self.searchBarPlaceholderColor = self.placeholderColor; - self.searchBarFont = nil; - self.searchBarSearchIconImage = nil; - self.searchBarClearIconImage = nil; self.searchBarTextFieldCornerRadius = 2.0; #pragma mark - TableView / TableViewCell self.tableViewEstimatedHeightEnabled = YES; - self.tableViewBackgroundColor = nil; - self.tableViewGroupedBackgroundColor = nil; - self.tableSectionIndexColor = nil; - self.tableSectionIndexBackgroundColor = nil; - self.tableSectionIndexTrackingBackgroundColor = nil; self.tableViewSeparatorColor = self.separatorColor; self.tableViewCellNormalHeight = UITableViewAutomaticDimension; - self.tableViewCellTitleLabelColor = nil; - self.tableViewCellDetailLabelColor = nil; - self.tableViewCellBackgroundColor = self.whiteColor; self.tableViewCellSelectedBackgroundColor = UIColorMake(238, 239, 241); self.tableViewCellWarningBackgroundColor = self.yellowColor; - self.tableViewCellDisclosureIndicatorImage = nil; - self.tableViewCellCheckmarkImage = nil; - self.tableViewCellDetailButtonImage = nil; self.tableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator = 12; self.tableViewSectionHeaderBackgroundColor = UIColorMake(244, 244, 244); @@ -250,7 +216,12 @@ - (void)initDefaultConfiguration { self.tableViewSectionFooterAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); self.tableViewSectionHeaderContentInset = UIEdgeInsetsMake(4, 15, 4, 15); self.tableViewSectionFooterContentInset = UIEdgeInsetsMake(4, 15, 4, 15); + if (@available(iOS 15.0, *)) { + self.tableViewSectionHeaderTopPadding = UITableViewAutomaticDimension; + } + + self.tableViewGroupedSeparatorColor = self.tableViewSeparatorColor; self.tableViewGroupedSectionHeaderFont = UIFontMake(12); self.tableViewGroupedSectionFooterFont = UIFontMake(12); self.tableViewGroupedSectionHeaderTextColor = self.grayDarkenColor; @@ -261,126 +232,416 @@ - (void)initDefaultConfiguration { self.tableViewGroupedSectionFooterDefaultHeight = UITableViewAutomaticDimension; self.tableViewGroupedSectionHeaderContentInset = UIEdgeInsetsMake(16, 15, 8, 15); self.tableViewGroupedSectionFooterContentInset = UIEdgeInsetsMake(8, 15, 2, 15); + if (@available(iOS 15.0, *)) { + self.tableViewInsetGroupedSectionHeaderTopPadding = UITableViewAutomaticDimension; + } + + self.tableViewInsetGroupedCornerRadius = 10; + self.tableViewInsetGroupedHorizontalInset = PreferredValueForVisualDevice(20, 15); + self.tableViewInsetGroupedSeparatorColor = self.tableViewSeparatorColor; + self.tableViewInsetGroupedSectionHeaderFont = self.tableViewGroupedSectionHeaderFont; + self.tableViewInsetGroupedSectionFooterFont = self.tableViewGroupedSectionFooterFont; + self.tableViewInsetGroupedSectionHeaderTextColor = self.tableViewSectionHeaderTextColor; + self.tableViewInsetGroupedSectionFooterTextColor = self.tableViewGroupedSectionFooterTextColor; + self.tableViewInsetGroupedSectionHeaderAccessoryMargins = self.tableViewGroupedSectionHeaderAccessoryMargins; + self.tableViewInsetGroupedSectionFooterAccessoryMargins = self.tableViewGroupedSectionFooterAccessoryMargins; + self.tableViewInsetGroupedSectionHeaderDefaultHeight = self.tableViewGroupedSectionHeaderDefaultHeight; + self.tableViewInsetGroupedSectionFooterDefaultHeight = self.tableViewGroupedSectionFooterDefaultHeight; + self.tableViewInsetGroupedSectionHeaderContentInset = self.tableViewGroupedSectionHeaderContentInset; + self.tableViewInsetGroupedSectionFooterContentInset = self.tableViewGroupedSectionFooterContentInset; + if (@available(iOS 15.0, *)) { + self.tableViewInsetGroupedSectionHeaderTopPadding = UITableViewAutomaticDimension; + } #pragma mark - UIWindowLevel self.windowLevelQMUIAlertView = UIWindowLevelAlert - 4.0; + self.windowLevelQMUIConsole = 1; #pragma mark - QMUILog self.shouldPrintDefaultLog = YES; self.shouldPrintInfoLog = YES; self.shouldPrintWarnLog = YES; + self.shouldPrintQMUIWarnLogToConsole = IS_DEBUG && !IS_XCTEST; + + #pragma mark - QMUIBadge + self.badgeOffset = CGPointMake(-9, 11); + self.badgeOffsetLandscape = CGPointMake(-9, 6); + self.updatesIndicatorSize = CGSizeMake(7, 7); + self.updatesIndicatorOffset = CGPointMake(4, self.updatesIndicatorSize.height); + self.updatesIndicatorOffsetLandscape = self.updatesIndicatorOffset; #pragma mark - Others - self.automaticCustomNavigationBarTransitionStyle = NO; self.supportedOrientationMask = UIInterfaceOrientationMaskAll; - self.automaticallyRotateDeviceOrientation = NO; - self.statusbarStyleLightInitially = NO; - self.needsBackBarButtonItemTitle = NO; - self.hidesBottomBarWhenPushedInitially = NO; + self.needsBackBarButtonItemTitle = YES; self.preventConcurrentNavigationControllerTransitions = YES; - self.navigationBarHiddenInitially = NO; - self.shouldFixTabBarTransitionBugInIPhoneX = NO; - self.shouldFixTabBarButtonBugForAll = NO; - self.shouldPrintQMUIWarnLogToConsole = IS_DEBUG; - self.sendAnalyticsToQMUITeam = YES; + self.shouldFixTabBarSafeAreaInsetsBug = YES; +} + +#pragma mark - Switch Setter + +/// 对 UIAppearance 设置一次 image 属性,在升起第三方键盘时就会执行一次 -[UIImage initWithCoder:],不管每次设置的是否是相同的对象,因此这里做一次值是否有变化的判断,尽量减少 UIAppearance 的设置。 +/// 注意,由于 QMUIConfiguration 里的 property setter 不仅是 retain 值,还起到刷新界面的作用,因此只有 QMUIThemeImage、QMUIThemeColor 等会“在 theme 变化时自动刷新”的对象才能用这个方法,其他类型的数据请自行检查 setter 里的逻辑是否需要在每次都调用。 +/// https://github.com/Tencent/QMUI_iOS/issues/1281 ++ (void)performAction:(void (NS_NOESCAPE ^)(void))action ifValueChanged:(id)oldValue newValue:(id)newValue { + if (!action) return; + BOOL valueChanged = newValue != oldValue; + if ([newValue isKindOfClass:NSValue.class] + || [newValue isKindOfClass:UIFont.class] + || ([newValue isKindOfClass:UIColor.class] && !((UIColor *)newValue).qmui_isQMUIDynamicColor)) { + valueChanged = ![newValue isEqual:oldValue]; + } + if (valueChanged) { + action(); + } +} + +- (void)setSwitchOnTintColor:(UIColor *)switchOnTintColor { + [QMUIConfiguration performAction:^{ + _switchOnTintColor = switchOnTintColor; + if (QMUIHelper.canUpdateAppearance) { + [UISwitch appearance].onTintColor = switchOnTintColor; + } + } ifValueChanged:_switchOnTintColor newValue:switchOnTintColor]; +} + +- (void)setSwitchThumbTintColor:(UIColor *)switchThumbTintColor { + [QMUIConfiguration performAction:^{ + _switchThumbTintColor = switchThumbTintColor; + if (QMUIHelper.canUpdateAppearance) { + [UISwitch appearance].thumbTintColor = switchThumbTintColor; + } + } ifValueChanged:_switchThumbTintColor newValue:switchThumbTintColor]; +} + +#pragma mark - NavigationBar Setter + +- (UINavigationBarAppearance *)navigationBarAppearance { + if (!_navigationBarAppearance) { + _navigationBarAppearance = [[UINavigationBarAppearance alloc] init]; + [_navigationBarAppearance configureWithDefaultBackground]; + } + return _navigationBarAppearance; +} + +- (void)updateNavigationBarBarAppearance { +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + if (QMUIHelper.canUpdateAppearance) { + UINavigationBar.qmui_appearanceConfigured.standardAppearance = self.navigationBarAppearance; + if (QMUICMIActivated && NavBarUsesStandardAppearanceOnly) { + UINavigationBar.qmui_appearanceConfigured.scrollEdgeAppearance = self.navigationBarAppearance; + } + } + } +#endif } - (void)setNavBarButtonFont:(UIFont *)navBarButtonFont { - _navBarButtonFont = navBarButtonFont; - // by molice 2017-08-04 只要用 appearence 的方式修改 UIBarButtonItem 的 font,就会导致界面切换时 UIBarButtonItem 抖动,系统的问题,所以暂时不修改 appearance。 - // by molice 2018-06-14 iOS 11 观察貌似又没抖动了,先试试看 - if (navBarButtonFont) { - UIBarButtonItem *barButtonItemAppearance = [UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UINavigationBar class]]]; - NSDictionary *attributes = @{NSFontAttributeName: navBarButtonFont}; - [barButtonItemAppearance setTitleTextAttributes:attributes forState:UIControlStateNormal]; - [barButtonItemAppearance setTitleTextAttributes:attributes forState:UIControlStateHighlighted]; - [barButtonItemAppearance setTitleTextAttributes:attributes forState:UIControlStateDisabled]; + [QMUIConfiguration performAction:^{ + _navBarButtonFont = navBarButtonFont; +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + NSMutableDictionary *titleTextAttributes = self.navigationBarAppearance.buttonAppearance.normal.titleTextAttributes.mutableCopy; + titleTextAttributes[NSFontAttributeName] = navBarButtonFont; + self.navigationBarAppearance.buttonAppearance.normal.titleTextAttributes = titleTextAttributes; + [self updateNavigationBarBarAppearance]; + } else { +#endif + // by molice 2017-08-04 只要用 appearence 的方式修改 UIBarButtonItem 的 font,就会导致界面切换时 UIBarButtonItem 抖动,系统的问题,所以暂时不修改 appearance。 + // by molice 2018-06-14 iOS 11 观察貌似又没抖动了,先试试看 + + if (QMUIHelper.canUpdateAppearance) { + UIBarButtonItem *barButtonItemAppearance = [UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UINavigationBar class]]]; + NSDictionary *attributes = navBarButtonFont ? @{NSFontAttributeName: navBarButtonFont} : nil; + [barButtonItemAppearance setTitleTextAttributes:attributes forState:UIControlStateNormal]; + [barButtonItemAppearance setTitleTextAttributes:attributes forState:UIControlStateHighlighted]; + [barButtonItemAppearance setTitleTextAttributes:attributes forState:UIControlStateDisabled]; + } +#ifdef IOS15_SDK_ALLOWED + } +#endif + } ifValueChanged:_navBarButtonFont newValue:navBarButtonFont]; +} + +- (void)setNavBarButtonFontBold:(UIFont *)navBarButtonFontBold { + // iOS 15 以前无法专门对 Done 类型设置样式,所以这里只对 iOS 15 生效 + if (@available(iOS 15.0, *)) { + [QMUIConfiguration performAction:^{ + _navBarButtonFontBold = navBarButtonFontBold; +#ifdef IOS15_SDK_ALLOWED + NSMutableDictionary *titleTextAttributes = self.navigationBarAppearance.doneButtonAppearance.normal.titleTextAttributes.mutableCopy; + titleTextAttributes[NSFontAttributeName] = navBarButtonFontBold; + self.navigationBarAppearance.doneButtonAppearance.normal.titleTextAttributes = titleTextAttributes; + [self updateNavigationBarBarAppearance]; +#endif + } ifValueChanged:_navBarButtonFontBold newValue:navBarButtonFontBold]; } } - (void)setNavBarTintColor:(UIColor *)navBarTintColor { _navBarTintColor = navBarTintColor; - if (navBarTintColor) { - [QMUIHelper visibleViewController].navigationController.navigationBar.tintColor = _navBarTintColor; - } + // tintColor 并没有声明 UI_APPEARANCE_SELECTOR,所以暂不使用 appearance 的方式去修改(虽然 appearance 方式实测是生效的) + [self.appearanceUpdatingNavigationControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController,NSUInteger idx, BOOL * _Nonnull stop) { + if (![navigationController.topViewController respondsToSelector:@selector(qmui_navigationBarTintColor)]) { + navigationController.navigationBar.tintColor = _navBarTintColor; + } + }]; } - (void)setNavBarBarTintColor:(UIColor *)navBarBarTintColor { - _navBarBarTintColor = navBarBarTintColor; - if (navBarBarTintColor) { - [UINavigationBar appearance].barTintColor = _navBarBarTintColor; - [QMUIHelper visibleViewController].navigationController.navigationBar.barTintColor = _navBarBarTintColor; - } + [QMUIConfiguration performAction:^{ + _navBarBarTintColor = navBarBarTintColor; + + // iOS 15 虽然不通过旧 API 设置样式,但 QMUI 里会从 appearance 的旧 API 取值作为默认值,所以这里不做 if iOS 15 的屏蔽。 + if (QMUIHelper.canUpdateAppearance) { + UINavigationBar.qmui_appearanceConfigured.barTintColor = navBarBarTintColor; + } + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + self.navigationBarAppearance.backgroundColor = navBarBarTintColor; + [self updateNavigationBarBarAppearance]; + } +#endif + + [self.appearanceUpdatingNavigationControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController,NSUInteger idx, BOOL * _Nonnull stop) { + if (![navigationController.topViewController respondsToSelector:@selector(qmui_navigationBarBarTintColor)]) { + navigationController.navigationBar.barTintColor = navBarBarTintColor; + } + }]; + } ifValueChanged:_navBarBarTintColor newValue:navBarBarTintColor]; } - (void)setNavBarShadowImage:(UIImage *)navBarShadowImage { - _navBarShadowImage = navBarShadowImage; - if (navBarShadowImage) { - [UINavigationBar appearance].shadowImage = _navBarShadowImage; - [QMUIHelper visibleViewController].navigationController.navigationBar.shadowImage = _navBarShadowImage; + [QMUIConfiguration performAction:^{ + _navBarShadowImage = navBarShadowImage; + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + self.navigationBarAppearance.shadowImage = navBarShadowImage; + [self updateNavigationBarBarAppearance]; + } +#endif + + // iOS 15 虽然不通过旧 API 设置样式,但 QMUI 里会从 appearance 的旧 API 取值作为默认值,所以这里不做 if iOS 15 的屏蔽。 + [self configureNavBarShadowImage]; + + } ifValueChanged:_navBarShadowImage newValue:!navBarShadowImage ? _navBarShadowImage : navBarShadowImage];// NavBarShadowImage 特殊一点,因为它在 NavBarShadowImageColor 里又会被赋值,所以这里对常见的组合“image = nil && imageColor = xxx”做特殊处理,避免误以为 valueChanged +} + +- (void)setNavBarShadowImageColor:(UIColor *)navBarShadowImageColor { + [QMUIConfiguration performAction:^{ + _navBarShadowImageColor = navBarShadowImageColor; + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + self.navigationBarAppearance.shadowColor = navBarShadowImageColor; + [self updateNavigationBarBarAppearance]; + } +#endif + // iOS 15 虽然不通过旧 API 设置样式,但 QMUI 里会从 appearance 的旧 API 取值作为默认值,所以这里不做 if iOS 15 的屏蔽。 + [self configureNavBarShadowImage]; + + } ifValueChanged:_navBarShadowImageColor newValue:navBarShadowImageColor]; +} + +- (void)configureNavBarShadowImage { + UIImage *shadowImage = self.navBarShadowImage; + if (shadowImage || self.navBarShadowImageColor) { + if (shadowImage) { + if (self.navBarShadowImageColor && shadowImage.renderingMode != UIImageRenderingModeAlwaysOriginal) { + shadowImage = [shadowImage qmui_imageWithTintColor:self.navBarShadowImageColor]; + } + } else { + shadowImage = [UIImage qmui_imageWithColor:self.navBarShadowImageColor size:CGSizeMake(4, PixelOne) cornerRadius:0]; + } + + // 反向更新 NavBarShadowImage,以保证业务代码直接使用 NavBarShadowImage 宏能得到正确的图片 + _navBarShadowImage = shadowImage; } + + if (QMUIHelper.canUpdateAppearance) { + UINavigationBar.qmui_appearanceConfigured.shadowImage = shadowImage; + } + + [self.appearanceUpdatingNavigationControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController,NSUInteger idx, BOOL * _Nonnull stop) { + if (![navigationController.topViewController respondsToSelector:@selector(qmui_navigationBarShadowImage)]) { + navigationController.navigationBar.shadowImage = shadowImage; + } + }]; +} + +- (void)setNavBarStyle:(UIBarStyle)navBarStyle { + [QMUIConfiguration performAction:^{ + _navBarStyle = navBarStyle; + + // iOS 15 虽然不通过旧 API 设置样式,但 QMUI 里会从 appearance 的旧 API 取值作为默认值,所以这里不做 if iOS 15 的屏蔽。 + if (QMUIHelper.canUpdateAppearance) { + UINavigationBar.qmui_appearanceConfigured.barStyle = navBarStyle; + } + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + self.navigationBarAppearance.backgroundEffect = [UIBlurEffect effectWithStyle:navBarStyle == UIBarStyleDefault ? UIBlurEffectStyleSystemChromeMaterialLight : UIBlurEffectStyleSystemChromeMaterialDark]; + [self updateNavigationBarBarAppearance]; + } +#endif + + [self.appearanceUpdatingNavigationControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController,NSUInteger idx, BOOL * _Nonnull stop) { + if (![navigationController.topViewController respondsToSelector:@selector(qmui_navigationBarStyle)]) { + navigationController.navigationBar.barStyle = navBarStyle; + } + }]; + } ifValueChanged:@(_navBarStyle) newValue:@(navBarStyle)]; } - (void)setNavBarBackgroundImage:(UIImage *)navBarBackgroundImage { - _navBarBackgroundImage = navBarBackgroundImage; - if (navBarBackgroundImage) { - [[UINavigationBar appearance] setBackgroundImage:_navBarBackgroundImage forBarMetrics:UIBarMetricsDefault]; - [[QMUIHelper visibleViewController].navigationController.navigationBar setBackgroundImage:_navBarBackgroundImage forBarMetrics:UIBarMetricsDefault]; - } + [QMUIConfiguration performAction:^{ + _navBarBackgroundImage = navBarBackgroundImage; + + // iOS 15 虽然不通过旧 API 设置样式,但 QMUI 里会从 appearance 的旧 API 取值作为默认值,所以这里不做 if iOS 15 的屏蔽。 + if (QMUIHelper.canUpdateAppearance) { + [UINavigationBar.qmui_appearanceConfigured setBackgroundImage:navBarBackgroundImage forBarMetrics:UIBarMetricsDefault]; + } + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + self.navigationBarAppearance.backgroundImage = navBarBackgroundImage; + [self updateNavigationBarBarAppearance]; + } +#endif + + [self.appearanceUpdatingNavigationControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController,NSUInteger idx, BOOL * _Nonnull stop) { + if (![navigationController.topViewController respondsToSelector:@selector(qmui_navigationBarBackgroundImage)]) { + [navigationController.navigationBar setBackgroundImage:navBarBackgroundImage forBarMetrics:UIBarMetricsDefault]; + } + }]; + } ifValueChanged:_navBarBackgroundImage newValue:navBarBackgroundImage]; } - (void)setNavBarTitleFont:(UIFont *)navBarTitleFont { - _navBarTitleFont = navBarTitleFont; - [self updateNavigationBarTitleAttributesIfNeeded]; + [QMUIConfiguration performAction:^{ + _navBarTitleFont = navBarTitleFont; + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + NSMutableDictionary *titleTextAttributes = self.navigationBarAppearance.titleTextAttributes.mutableCopy; + titleTextAttributes[NSFontAttributeName] = navBarTitleFont; + self.navigationBarAppearance.titleTextAttributes = titleTextAttributes; + [self updateNavigationBarBarAppearance]; + } +#endif + + // iOS 15 虽然不通过旧 API 设置样式,但 QMUI 里会从 appearance 的旧 API 取值作为默认值,所以这里不做 if iOS 15 的屏蔽。 + [self updateNavigationBarTitleAttributesIfNeeded]; + } ifValueChanged:_navBarTitleFont newValue:navBarTitleFont]; } - (void)setNavBarTitleColor:(UIColor *)navBarTitleColor { - _navBarTitleColor = navBarTitleColor; - [self updateNavigationBarTitleAttributesIfNeeded]; + [QMUIConfiguration performAction:^{ + _navBarTitleColor = navBarTitleColor; + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + NSMutableDictionary *titleTextAttributes = self.navigationBarAppearance.titleTextAttributes.mutableCopy; + titleTextAttributes[NSForegroundColorAttributeName] = navBarTitleColor; + self.navigationBarAppearance.titleTextAttributes = titleTextAttributes; + [self updateNavigationBarBarAppearance]; + } +#endif + + // iOS 15 虽然不通过旧 API 设置样式,但 QMUI 里会从 appearance 的旧 API 取值作为默认值,所以这里不做 if iOS 15 的屏蔽。 + [self updateNavigationBarTitleAttributesIfNeeded]; + } ifValueChanged:_navBarTitleColor newValue:navBarTitleColor]; } - (void)updateNavigationBarTitleAttributesIfNeeded { - if (self.navBarTitleFont || self.navBarTitleColor) { - NSMutableDictionary *titleTextAttributes = [UINavigationBar appearance].titleTextAttributes.mutableCopy; - if (!titleTextAttributes) { - titleTextAttributes = [[NSMutableDictionary alloc] init]; - } - if (self.navBarTitleFont) { - titleTextAttributes[NSFontAttributeName] = self.navBarTitleFont; - } - if (self.navBarTitleColor) { - titleTextAttributes[NSForegroundColorAttributeName] = self.navBarTitleColor; - } - [UINavigationBar appearance].titleTextAttributes = titleTextAttributes; - [QMUIHelper visibleViewController].navigationController.navigationBar.titleTextAttributes = titleTextAttributes; + NSMutableDictionary *titleTextAttributes = UINavigationBar.qmui_appearanceConfigured.titleTextAttributes.mutableCopy; + if (!titleTextAttributes) { + titleTextAttributes = [[NSMutableDictionary alloc] init]; + } + if (self.navBarTitleFont) { + titleTextAttributes[NSFontAttributeName] = self.navBarTitleFont; + } + if (self.navBarTitleColor) { + titleTextAttributes[NSForegroundColorAttributeName] = self.navBarTitleColor; } + if (QMUIHelper.canUpdateAppearance) { + UINavigationBar.qmui_appearanceConfigured.titleTextAttributes = titleTextAttributes; + } +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + } else { +#endif + [self.appearanceUpdatingNavigationControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController,NSUInteger idx, BOOL * _Nonnull stop) { + if (![navigationController.topViewController respondsToSelector:@selector(qmui_titleViewTintColor)]) { + navigationController.navigationBar.titleTextAttributes = titleTextAttributes; + } + }]; +#ifdef IOS15_SDK_ALLOWED + } +#endif } - (void)setNavBarLargeTitleFont:(UIFont *)navBarLargeTitleFont { - _navBarLargeTitleFont = navBarLargeTitleFont; - [self updateNavigationBarLargeTitleTextAttributesIfNeeded]; + [QMUIConfiguration performAction:^{ + _navBarLargeTitleFont = navBarLargeTitleFont; + + // iOS 15 虽然不通过旧 API 设置样式,但 QMUI 里会从 appearance 的旧 API 取值作为默认值,所以这里不做 if iOS 15 的屏蔽。 + [self updateNavigationBarLargeTitleTextAttributesIfNeeded]; + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + NSMutableDictionary *largeTitleTextAttributes = self.navigationBarAppearance.largeTitleTextAttributes.mutableCopy; + largeTitleTextAttributes[NSFontAttributeName] = navBarLargeTitleFont; + self.navigationBarAppearance.largeTitleTextAttributes = largeTitleTextAttributes; + [self updateNavigationBarBarAppearance]; + } +#endif + } ifValueChanged:_navBarLargeTitleFont newValue:navBarLargeTitleFont]; } - (void)setNavBarLargeTitleColor:(UIColor *)navBarLargeTitleColor { - _navBarLargeTitleColor = navBarLargeTitleColor; - [self updateNavigationBarLargeTitleTextAttributesIfNeeded]; + [QMUIConfiguration performAction:^{ + _navBarLargeTitleColor = navBarLargeTitleColor; + + // iOS 15 虽然不通过旧 API 设置样式,但 QMUI 里会从 appearance 的旧 API 取值作为默认值,所以这里不做 if iOS 15 的屏蔽。 + [self updateNavigationBarLargeTitleTextAttributesIfNeeded]; + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + NSMutableDictionary *largeTitleTextAttributes = self.navigationBarAppearance.largeTitleTextAttributes.mutableCopy; + largeTitleTextAttributes[NSForegroundColorAttributeName] = navBarLargeTitleColor; + self.navigationBarAppearance.largeTitleTextAttributes = largeTitleTextAttributes; + [self updateNavigationBarBarAppearance]; + } +#endif + } ifValueChanged:_navBarLargeTitleColor newValue:navBarLargeTitleColor]; } - (void)updateNavigationBarLargeTitleTextAttributesIfNeeded { - if (@available(iOS 11, *)) { - if (self.navBarLargeTitleFont || self.navBarLargeTitleColor) { - NSMutableDictionary *largeTitleTextAttributes = [[NSMutableDictionary alloc] init]; - if (self.navBarLargeTitleFont) { - largeTitleTextAttributes[NSFontAttributeName] = self.navBarLargeTitleFont; - } - if (self.navBarLargeTitleColor) { - largeTitleTextAttributes[NSForegroundColorAttributeName] = self.navBarLargeTitleColor; - } - [UINavigationBar appearance].largeTitleTextAttributes = largeTitleTextAttributes; - [QMUIHelper visibleViewController].navigationController.navigationBar.largeTitleTextAttributes = largeTitleTextAttributes; - } + NSMutableDictionary *largeTitleTextAttributes = [[NSMutableDictionary alloc] init]; + if (self.navBarLargeTitleFont) { + largeTitleTextAttributes[NSFontAttributeName] = self.navBarLargeTitleFont; + } + if (self.navBarLargeTitleColor) { + largeTitleTextAttributes[NSForegroundColorAttributeName] = self.navBarLargeTitleColor; + } + if (QMUIHelper.canUpdateAppearance) { + UINavigationBar.qmui_appearanceConfigured.largeTitleTextAttributes = largeTitleTextAttributes; } + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + } else { +#endif + [self.appearanceUpdatingNavigationControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController,NSUInteger idx, BOOL * _Nonnull stop) { + navigationController.navigationBar.largeTitleTextAttributes = largeTitleTextAttributes; + }]; +#ifdef IOS15_SDK_ALLOWED + } +#endif } - (void)setSizeNavBarBackIndicatorImageAutomatically:(BOOL)sizeNavBarBackIndicatorImageAutomatically { @@ -391,15 +652,12 @@ - (void)setSizeNavBarBackIndicatorImageAutomatically:(BOOL)sizeNavBarBackIndicat } - (void)setNavBarBackIndicatorImage:(UIImage *)navBarBackIndicatorImage { - _navBarBackIndicatorImage = navBarBackIndicatorImage; - - if (_navBarBackIndicatorImage) { - UINavigationBar *navBarAppearance = [UINavigationBar appearance]; - UINavigationBar *navigationBar = [QMUIHelper visibleViewController].navigationController.navigationBar; + [QMUIConfiguration performAction:^{ + _navBarBackIndicatorImage = navBarBackIndicatorImage; // 返回按钮的图片frame是和系统默认的返回图片的大小一致的(13, 21),所以用自定义返回箭头时要保证图片大小与系统的箭头大小一样,否则无法对齐 // Make sure custom back button image is the same size as the system's back button image, i.e. (13, 21), due to the same frame size they share. - if (self.sizeNavBarBackIndicatorImageAutomatically) { + if (navBarBackIndicatorImage && self.sizeNavBarBackIndicatorImageAutomatically) { CGSize systemBackIndicatorImageSize = kUINavigationBarBackIndicatorImageSize; CGSize customBackIndicatorImageSize = _navBarBackIndicatorImage.size; if (!CGSizeEqualToSize(customBackIndicatorImageSize, systemBackIndicatorImageSize)) { @@ -410,122 +668,417 @@ - (void)setNavBarBackIndicatorImage:(UIImage *)navBarBackIndicatorImage { systemBackIndicatorImageSize.width - customBackIndicatorImageSize.width)] imageWithRenderingMode:_navBarBackIndicatorImage.renderingMode]; } } + + // iOS 15 虽然不通过旧 API 设置样式,但 QMUI 里会从 appearance 的旧 API 取值作为默认值,所以这里不做 if iOS 15 的屏蔽。 + if (QMUIHelper.canUpdateAppearance) { + UINavigationBar *navBarAppearance = UINavigationBar.qmui_appearanceConfigured; + navBarAppearance.backIndicatorImage = _navBarBackIndicatorImage; + navBarAppearance.backIndicatorTransitionMaskImage = _navBarBackIndicatorImage; + } - navBarAppearance.backIndicatorImage = _navBarBackIndicatorImage; - navBarAppearance.backIndicatorTransitionMaskImage = _navBarBackIndicatorImage; - navigationBar.backIndicatorImage = _navBarBackIndicatorImage; - navigationBar.backIndicatorTransitionMaskImage = _navBarBackIndicatorImage; - } +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + [self.navigationBarAppearance setBackIndicatorImage:_navBarBackIndicatorImage transitionMaskImage:_navBarBackIndicatorImage]; + [self updateNavigationBarBarAppearance]; + } +#endif + + [self.appearanceUpdatingNavigationControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController,NSUInteger idx, BOOL * _Nonnull stop) { + navigationController.navigationBar.backIndicatorImage = _navBarBackIndicatorImage; + navigationController.navigationBar.backIndicatorTransitionMaskImage = _navBarBackIndicatorImage; + }]; + } ifValueChanged:_navBarBackIndicatorImage newValue:navBarBackIndicatorImage]; } - (void)setNavBarBackButtonTitlePositionAdjustment:(UIOffset)navBarBackButtonTitlePositionAdjustment { - _navBarBackButtonTitlePositionAdjustment = navBarBackButtonTitlePositionAdjustment; - - if (!UIOffsetEqualToOffset(UIOffsetZero, _navBarBackButtonTitlePositionAdjustment)) { - UIBarButtonItem *backBarButtonItem = [UIBarButtonItem appearance]; - [backBarButtonItem setBackButtonTitlePositionAdjustment:_navBarBackButtonTitlePositionAdjustment forBarMetrics:UIBarMetricsDefault]; - [[QMUIHelper visibleViewController].navigationController.navigationItem.backBarButtonItem setBackButtonTitlePositionAdjustment:_navBarBackButtonTitlePositionAdjustment forBarMetrics:UIBarMetricsDefault]; + [QMUIConfiguration performAction:^{ + _navBarBackButtonTitlePositionAdjustment = navBarBackButtonTitlePositionAdjustment; + + // iOS 15 虽然不通过旧 API 设置样式,但 QMUI 里会从 appearance 的旧 API 取值作为默认值,所以这里不做 if iOS 15 的屏蔽。 + if (QMUIHelper.canUpdateAppearance) { + UIBarButtonItem *backBarButtonItem = [UIBarButtonItem appearance]; + [backBarButtonItem setBackButtonTitlePositionAdjustment:_navBarBackButtonTitlePositionAdjustment forBarMetrics:UIBarMetricsDefault]; + } + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + self.navigationBarAppearance.backButtonAppearance.normal.titlePositionAdjustment = navBarBackButtonTitlePositionAdjustment; + [self updateNavigationBarBarAppearance]; + } else { +#endif + [self.appearanceUpdatingNavigationControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController, NSUInteger idx, BOOL * _Nonnull stop) { + [navigationController.navigationItem.backBarButtonItem setBackButtonTitlePositionAdjustment:_navBarBackButtonTitlePositionAdjustment forBarMetrics:UIBarMetricsDefault]; + }]; +#ifdef IOS15_SDK_ALLOWED + } +#endif + } ifValueChanged:[NSValue valueWithUIOffset:_navBarBackButtonTitlePositionAdjustment] newValue:[NSValue valueWithUIOffset:navBarBackButtonTitlePositionAdjustment]]; +} + +#pragma mark - ToolBar Setter + +- (UIToolbarAppearance *)toolBarAppearance { + if (!_toolBarAppearance) { + _toolBarAppearance = [[UIToolbarAppearance alloc] init]; + [_toolBarAppearance configureWithDefaultBackground]; + } + return _toolBarAppearance; +} + +- (void)updateToolBarBarAppearance { +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + if (QMUIHelper.canUpdateAppearance) { + UIToolbar.qmui_appearanceConfigured.standardAppearance = self.toolBarAppearance; + if (QMUICMIActivated && ToolBarUsesStandardAppearanceOnly) { + UIToolbar.qmui_appearanceConfigured.scrollEdgeAppearance = self.toolBarAppearance; + } + } + [self.appearanceUpdatingToolbarControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController, NSUInteger idx, BOOL * _Nonnull stop) { + navigationController.toolbar.standardAppearance = self.toolBarAppearance; + if (QMUICMIActivated && ToolBarUsesStandardAppearanceOnly) { + navigationController.toolbar.scrollEdgeAppearance = self.toolBarAppearance; + } + }]; } +#endif } - (void)setToolBarTintColor:(UIColor *)toolBarTintColor { _toolBarTintColor = toolBarTintColor; - if (toolBarTintColor) { - [QMUIHelper visibleViewController].navigationController.toolbar.tintColor = _toolBarTintColor; - } + // tintColor 并没有声明 UI_APPEARANCE_SELECTOR,所以暂不使用 appearance 的方式去修改(虽然 appearance 方式实测是生效的) + [self.appearanceUpdatingNavigationControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController, NSUInteger idx, BOOL * _Nonnull stop) { + navigationController.toolbar.tintColor = _toolBarTintColor; + }]; +} + +- (void)setToolBarStyle:(UIBarStyle)toolBarStyle { + [QMUIConfiguration performAction:^{ + _toolBarStyle = toolBarStyle; + if (QMUIHelper.canUpdateAppearance) { + UIToolbar.qmui_appearanceConfigured.barStyle = toolBarStyle; + } + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + self.toolBarAppearance.backgroundEffect = [UIBlurEffect effectWithStyle:toolBarStyle == UIBarStyleDefault ? UIBlurEffectStyleSystemChromeMaterialLight : UIBlurEffectStyleSystemChromeMaterialDark]; + [self updateToolBarBarAppearance]; + } else { +#endif + [self.appearanceUpdatingToolbarControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController, NSUInteger idx, BOOL * _Nonnull stop) { + navigationController.toolbar.barStyle = toolBarStyle; + }]; +#ifdef IOS15_SDK_ALLOWED + } +#endif + } ifValueChanged:@(_toolBarStyle) newValue:@(toolBarStyle)]; } - (void)setToolBarBarTintColor:(UIColor *)toolBarBarTintColor { - _toolBarBarTintColor = toolBarBarTintColor; - if (toolBarBarTintColor) { - [UIToolbar appearance].barTintColor = _toolBarBarTintColor; - [QMUIHelper visibleViewController].navigationController.toolbar.barTintColor = _toolBarBarTintColor; - } + [QMUIConfiguration performAction:^{ + _toolBarBarTintColor = toolBarBarTintColor; + if (QMUIHelper.canUpdateAppearance) { + UIToolbar.qmui_appearanceConfigured.barTintColor = _toolBarBarTintColor; + } + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + self.toolBarAppearance.backgroundColor = toolBarBarTintColor; + [self updateToolBarBarAppearance]; + } else { +#endif + [self.appearanceUpdatingToolbarControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController, NSUInteger idx, BOOL * _Nonnull stop) { + navigationController.toolbar.barTintColor = _toolBarBarTintColor; + }]; +#ifdef IOS15_SDK_ALLOWED + } +#endif + } ifValueChanged:_toolBarBarTintColor newValue:toolBarBarTintColor]; } - (void)setToolBarBackgroundImage:(UIImage *)toolBarBackgroundImage { - _toolBarBackgroundImage = toolBarBackgroundImage; - if (toolBarBackgroundImage) { - [[UIToolbar appearance] setBackgroundImage:_toolBarBackgroundImage forToolbarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault]; - [[QMUIHelper visibleViewController].navigationController.toolbar setBackgroundImage:_toolBarBackgroundImage forToolbarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault]; - } + [QMUIConfiguration performAction:^{ + _toolBarBackgroundImage = toolBarBackgroundImage; + if (QMUIHelper.canUpdateAppearance) { + [UIToolbar.qmui_appearanceConfigured setBackgroundImage:_toolBarBackgroundImage forToolbarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault]; + } + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + self.toolBarAppearance.backgroundImage = toolBarBackgroundImage; + [self updateToolBarBarAppearance]; + } else { +#endif + [self.appearanceUpdatingToolbarControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController, NSUInteger idx, BOOL * _Nonnull stop) { + [navigationController.toolbar setBackgroundImage:_toolBarBackgroundImage forToolbarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault]; + }]; +#ifdef IOS15_SDK_ALLOWED + } +#endif + } ifValueChanged:_toolBarBackgroundImage newValue:toolBarBackgroundImage]; } - (void)setToolBarShadowImageColor:(UIColor *)toolBarShadowImageColor { - _toolBarShadowImageColor = toolBarShadowImageColor; - if (_toolBarShadowImageColor) { - UIImage *shadowImage = [UIImage qmui_imageWithColor:_toolBarShadowImageColor size:CGSizeMake(1, PixelOne) cornerRadius:0]; - [[UIToolbar appearance] setShadowImage:shadowImage forToolbarPosition:UIBarPositionAny]; - [[QMUIHelper visibleViewController].navigationController.toolbar setShadowImage:shadowImage forToolbarPosition:UIBarPositionAny]; + [QMUIConfiguration performAction:^{ + _toolBarShadowImageColor = toolBarShadowImageColor; + UIImage *shadowImage = toolBarShadowImageColor ? [UIImage qmui_imageWithColor:_toolBarShadowImageColor size:CGSizeMake(1, PixelOne) cornerRadius:0] : nil; + if (QMUIHelper.canUpdateAppearance) { + [UIToolbar.qmui_appearanceConfigured setShadowImage:shadowImage forToolbarPosition:UIBarPositionAny]; + } + +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + self.toolBarAppearance.shadowColor = toolBarShadowImageColor; + [self updateToolBarBarAppearance]; + } else { +#endif + [self.appearanceUpdatingToolbarControllers enumerateObjectsUsingBlock:^(UINavigationController * _Nonnull navigationController, NSUInteger idx, BOOL * _Nonnull stop) { + [navigationController.toolbar setShadowImage:shadowImage forToolbarPosition:UIBarPositionAny]; + }]; +#ifdef IOS15_SDK_ALLOWED + } +#endif + } ifValueChanged:_toolBarShadowImageColor newValue:toolBarShadowImageColor]; +} + +#pragma mark - TabBar Setter + +- (UITabBarAppearance *)tabBarAppearance { + if (!_tabBarAppearance) { + _tabBarAppearance = [[UITabBarAppearance alloc] init]; + [_tabBarAppearance configureWithDefaultBackground]; } + return _tabBarAppearance; } -- (void)setTabBarTintColor:(UIColor *)tabBarTintColor { - _tabBarTintColor = tabBarTintColor; - if (tabBarTintColor) { - [QMUIHelper visibleViewController].tabBarController.tabBar.tintColor = _tabBarTintColor; +- (void)updateTabBarAppearance { + if (QMUIHelper.canUpdateAppearance) { + UITabBar.qmui_appearanceConfigured.standardAppearance = self.tabBarAppearance; +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + if (QMUICMIActivated && TabBarUsesStandardAppearanceOnly) { + UITabBar.qmui_appearanceConfigured.scrollEdgeAppearance = self.tabBarAppearance; + } + } +#endif } + [self.appearanceUpdatingTabBarControllers enumerateObjectsUsingBlock:^(UITabBarController * _Nonnull tabBarController, NSUInteger idx, BOOL * _Nonnull stop) { + tabBarController.tabBar.standardAppearance = self.tabBarAppearance; +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + if (QMUICMIActivated && TabBarUsesStandardAppearanceOnly) { + tabBarController.tabBar.scrollEdgeAppearance = self.tabBarAppearance; + } + } +#endif + [tabBarController.tabBar setNeedsLayout];// theme 不跟随系统的情况下切换 Light/Dark,tabBarAppearance.backgroundEffect 虽然值被更新了,但样式被刷新,这里手动触发一下 + }]; } - (void)setTabBarBarTintColor:(UIColor *)tabBarBarTintColor { - _tabBarBarTintColor = tabBarBarTintColor; - if (tabBarBarTintColor) { - [UITabBar appearance].barTintColor = _tabBarBarTintColor; - [QMUIHelper visibleViewController].tabBarController.tabBar.barTintColor = _tabBarBarTintColor; - } + [QMUIConfiguration performAction:^{ + _tabBarBarTintColor = tabBarBarTintColor; + self.tabBarAppearance.backgroundColor = tabBarBarTintColor; + [self updateTabBarAppearance]; + } ifValueChanged:_tabBarBarTintColor newValue:tabBarBarTintColor]; +} + +- (void)setTabBarStyle:(UIBarStyle)tabBarStyle { + [QMUIConfiguration performAction:^{ + _tabBarStyle = tabBarStyle; + self.tabBarAppearance.backgroundEffect = [UIBlurEffect effectWithStyle:tabBarStyle == UIBarStyleDefault ? UIBlurEffectStyleSystemChromeMaterialLight : UIBlurEffectStyleSystemChromeMaterialDark]; + [self updateTabBarAppearance]; + } ifValueChanged:@(_tabBarStyle) newValue:@(tabBarStyle)]; } - (void)setTabBarBackgroundImage:(UIImage *)tabBarBackgroundImage { - _tabBarBackgroundImage = tabBarBackgroundImage; - if (tabBarBackgroundImage) { - [UITabBar appearance].backgroundImage = _tabBarBackgroundImage; - [QMUIHelper visibleViewController].tabBarController.tabBar.backgroundImage = _tabBarBackgroundImage; - } + [QMUIConfiguration performAction:^{ + _tabBarBackgroundImage = tabBarBackgroundImage; + self.tabBarAppearance.backgroundImage = tabBarBackgroundImage; + [self updateTabBarAppearance]; + } ifValueChanged:_tabBarBackgroundImage newValue:tabBarBackgroundImage]; } - (void)setTabBarShadowImageColor:(UIColor *)tabBarShadowImageColor { - _tabBarShadowImageColor = tabBarShadowImageColor; - if (_tabBarShadowImageColor) { - UIImage *shadowImage = [UIImage qmui_imageWithColor:_tabBarShadowImageColor size:CGSizeMake(1, PixelOne) cornerRadius:0]; - [[UITabBar appearance] setShadowImage:shadowImage]; - [QMUIHelper visibleViewController].tabBarController.tabBar.shadowImage = shadowImage; - } + [QMUIConfiguration performAction:^{ + _tabBarShadowImageColor = tabBarShadowImageColor; + self.tabBarAppearance.shadowColor = tabBarShadowImageColor; + [self updateTabBarAppearance]; + } ifValueChanged:_tabBarShadowImageColor newValue:tabBarShadowImageColor]; } -- (void)setTabBarItemTitleColor:(UIColor *)tabBarItemTitleColor { - _tabBarItemTitleColor = tabBarItemTitleColor; - if (_tabBarItemTitleColor) { - NSMutableDictionary *textAttributes = [[NSMutableDictionary alloc] initWithDictionary:[[UITabBarItem appearance] titleTextAttributesForState:UIControlStateNormal]]; - textAttributes[NSForegroundColorAttributeName] = _tabBarItemTitleColor; - [[UITabBarItem appearance] setTitleTextAttributes:textAttributes forState:UIControlStateNormal]; - [[QMUIHelper visibleViewController].tabBarController.tabBar.items enumerateObjectsUsingBlock:^(UITabBarItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - [obj setTitleTextAttributes:textAttributes forState:UIControlStateNormal]; +- (void)setTabBarItemTitleFont:(UIFont *)tabBarItemTitleFont { + [QMUIConfiguration performAction:^{ + _tabBarItemTitleFont = tabBarItemTitleFont; + [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) { + NSMutableDictionary *attributes = itemAppearance.normal.titleTextAttributes.mutableCopy; + attributes[NSFontAttributeName] = tabBarItemTitleFont; + itemAppearance.normal.titleTextAttributes = attributes.copy; }]; - } + [self updateTabBarAppearance]; + } ifValueChanged:_tabBarItemTitleFont newValue:tabBarItemTitleFont]; } -- (void)setTabBarItemTitleFont:(UIFont *)tabBarItemTitleFont { - _tabBarItemTitleFont = tabBarItemTitleFont; - if (_tabBarItemTitleFont) { - NSMutableDictionary *textAttributes = [[NSMutableDictionary alloc] initWithDictionary:[[UITabBarItem appearance] titleTextAttributesForState:UIControlStateNormal]]; - textAttributes[NSFontAttributeName] = _tabBarItemTitleFont; - [[UITabBarItem appearance] setTitleTextAttributes:textAttributes forState:UIControlStateNormal]; - [[QMUIHelper visibleViewController].tabBarController.tabBar.items enumerateObjectsUsingBlock:^(UITabBarItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - [obj setTitleTextAttributes:textAttributes forState:UIControlStateNormal]; +- (void)setTabBarItemTitleFontSelected:(UIFont *)tabBarItemTitleFontSelected { + [QMUIConfiguration performAction:^{ + _tabBarItemTitleFontSelected = tabBarItemTitleFontSelected; + [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) { + NSMutableDictionary *attributes = itemAppearance.selected.titleTextAttributes.mutableCopy; + attributes[NSFontAttributeName] = tabBarItemTitleFontSelected; + itemAppearance.selected.titleTextAttributes = attributes.copy; }]; - } + [self updateTabBarAppearance]; + } ifValueChanged:_tabBarItemTitleFontSelected newValue:tabBarItemTitleFontSelected]; +} + +- (void)setTabBarItemTitleColor:(UIColor *)tabBarItemTitleColor { + [QMUIConfiguration performAction:^{ + _tabBarItemTitleColor = tabBarItemTitleColor; + [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) { + NSMutableDictionary *attributes = itemAppearance.normal.titleTextAttributes.mutableCopy; + attributes[NSForegroundColorAttributeName] = tabBarItemTitleColor; + itemAppearance.normal.titleTextAttributes = attributes.copy; + }]; + [self updateTabBarAppearance]; + } ifValueChanged:_tabBarItemTitleColor newValue:tabBarItemTitleColor]; } - (void)setTabBarItemTitleColorSelected:(UIColor *)tabBarItemTitleColorSelected { - _tabBarItemTitleColorSelected = tabBarItemTitleColorSelected; - if (_tabBarItemTitleColorSelected) { - NSMutableDictionary *textAttributes = [[NSMutableDictionary alloc] initWithDictionary:[[UITabBarItem appearance] titleTextAttributesForState:UIControlStateSelected]]; - textAttributes[NSForegroundColorAttributeName] = _tabBarItemTitleColorSelected; - [[UITabBarItem appearance] setTitleTextAttributes:textAttributes forState:UIControlStateSelected]; - [[QMUIHelper visibleViewController].tabBarController.tabBar.items enumerateObjectsUsingBlock:^(UITabBarItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - [obj setTitleTextAttributes:textAttributes forState:UIControlStateSelected]; + [QMUIConfiguration performAction:^{ + _tabBarItemTitleColorSelected = tabBarItemTitleColorSelected; + [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) { + NSMutableDictionary *attributes = itemAppearance.selected.titleTextAttributes.mutableCopy; + attributes[NSForegroundColorAttributeName] = tabBarItemTitleColorSelected; + itemAppearance.selected.titleTextAttributes = attributes.copy; }]; + [self updateTabBarAppearance]; + } ifValueChanged:_tabBarItemTitleColorSelected newValue:tabBarItemTitleColorSelected]; +} + +- (void)setTabBarItemImageColor:(UIColor *)tabBarItemImageColor { + [QMUIConfiguration performAction:^{ + _tabBarItemImageColor = tabBarItemImageColor; + [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) { + itemAppearance.normal.iconColor = tabBarItemImageColor; + }]; + [self updateTabBarAppearance]; + } ifValueChanged:_tabBarItemImageColor newValue:tabBarItemImageColor]; +} + +- (void)setTabBarItemImageColorSelected:(UIColor *)tabBarItemImageColorSelected { + [QMUIConfiguration performAction:^{ + _tabBarItemImageColorSelected = tabBarItemImageColorSelected; + [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) { + itemAppearance.selected.iconColor = tabBarItemImageColorSelected; + }]; + [self updateTabBarAppearance]; + } ifValueChanged:_tabBarItemImageColorSelected newValue:tabBarItemImageColorSelected]; +} + +- (void)setDefaultStatusBarStyle:(UIStatusBarStyle)defaultStatusBarStyle { + _defaultStatusBarStyle = defaultStatusBarStyle; + [[QMUIHelper visibleViewController] setNeedsStatusBarAppearanceUpdate]; +} + +#pragma mark - Appearance Updating Views + +// 解决某些场景下更新配置表无法覆盖样式的问题 https://github.com/Tencent/QMUI_iOS/issues/700 + +- (NSArray *)appearanceUpdatingTabBarControllers { + NSArray> *classes = nil; + if (self.tabBarContainerClasses.count > 0) { + classes = self.tabBarContainerClasses; + } else { + classes = @[UITabBarController.class]; + } + // tabBarContainerClasses 里可能会设置非 UITabBarController 的 class,由于这里只需要关注 UITabBarController 的,所以做一次过滤 + classes = [classes qmui_filterWithBlock:^BOOL(Class _Nonnull item) { + return [item.class isSubclassOfClass:UITabBarController.class]; + }]; + return (NSArray *)[self appearanceUpdatingViewControllersOfClasses:classes]; +} + +- (NSArray *)appearanceUpdatingNavigationControllers { + NSArray> *classes = nil; + if (self.navBarContainerClasses.count > 0) { + classes = self.navBarContainerClasses; + } else { + classes = @[UINavigationController.class]; + } + // navBarContainerClasses 里可能会设置非 UINavigationController 的 class,由于这里只需要关注 UINavigationController 的,所以做一次过滤 + classes = [classes qmui_filterWithBlock:^BOOL(Class _Nonnull item) { + return [item.class isSubclassOfClass:UINavigationController.class]; + }]; + return (NSArray *)[self appearanceUpdatingViewControllersOfClasses:classes]; +} + +- (NSArray *)appearanceUpdatingToolbarControllers { + NSArray> *classes = nil; + if (self.toolBarContainerClasses.count > 0) { + classes = self.toolBarContainerClasses; + } else { + classes = @[UINavigationController.class]; + } + // toolBarContainerClasses 里可能会设置非 UINavigationController 的 class,由于这里只需要关注 UINavigationController 的,所以做一次过滤 + classes = [classes qmui_filterWithBlock:^BOOL(Class _Nonnull item) { + return [item.class isSubclassOfClass:UINavigationController.class]; + }]; + return (NSArray *)[self appearanceUpdatingViewControllersOfClasses:classes]; +} + +- (NSArray *)appearanceUpdatingViewControllersOfClasses:(NSArray> *)classes { + if (!classes.count) return nil; + NSMutableArray *viewControllers = [NSMutableArray array]; + [UIApplication.sharedApplication.windows enumerateObjectsUsingBlock:^(__kindof UIWindow * _Nonnull window, NSUInteger idx, BOOL * _Nonnull stop) { + if (window.rootViewController) { + [viewControllers addObjectsFromArray:[window.rootViewController qmui_existingViewControllersOfClasses:classes]]; + } + }]; + return viewControllers; +} + +@end + +@implementation UINavigationBar (QMUIConfiguration) + ++ (instancetype)qmui_appearanceConfigured { + if (QMUICMIActivated && NavBarContainerClasses) { + return [self appearanceWhenContainedInInstancesOfClasses:NavBarContainerClasses]; + } + return [self appearance]; +} + +@end + +@implementation UITabBar (QMUIConfiguration) + ++ (instancetype)qmui_appearanceConfigured { + if (QMUICMIActivated && TabBarContainerClasses) { + return [self appearanceWhenContainedInInstancesOfClasses:TabBarContainerClasses]; + } + return [self appearance]; +} + +@end + +@implementation UIToolbar (QMUIConfiguration) + ++ (instancetype)qmui_appearanceConfigured { + if (QMUICMIActivated && ToolBarContainerClasses) { + return [self appearanceWhenContainedInInstancesOfClasses:ToolBarContainerClasses]; + } + return [self appearance]; +} + +@end + +@implementation UITabBarItem (QMUIConfiguration) + ++ (instancetype)qmui_appearanceConfigured { + if (QMUICMIActivated && TabBarContainerClasses) { + return [self appearanceWhenContainedInInstancesOfClasses:TabBarContainerClasses]; } + return [self appearance]; } @end diff --git a/QMUI/QMUIKit/QMUICore/QMUIConfigurationMacros.h b/QMUI/QMUIKit/QMUICore/QMUIConfigurationMacros.h index e9f7dbdb..2ecfa753 100644 --- a/QMUI/QMUIKit/QMUICore/QMUIConfigurationMacros.h +++ b/QMUI/QMUIKit/QMUICore/QMUIConfigurationMacros.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIConfigurationMacros.h @@ -71,33 +71,31 @@ #define ButtonDisabledAlpha [QMUICMI buttonDisabledAlpha] // 按钮Disabled状态的透明度 #define ButtonTintColor [QMUICMI buttonTintColor] // 普通按钮的颜色 -#define GhostButtonColorBlue [QMUICMI ghostButtonColorBlue] // QMUIGhostButtonColorBlue的颜色 -#define GhostButtonColorRed [QMUICMI ghostButtonColorRed] // QMUIGhostButtonColorRed的颜色 -#define GhostButtonColorGreen [QMUICMI ghostButtonColorGreen] // QMUIGhostButtonColorGreen的颜色 -#define GhostButtonColorGray [QMUICMI ghostButtonColorGray] // QMUIGhostButtonColorGray的颜色 -#define GhostButtonColorWhite [QMUICMI ghostButtonColorWhite] // QMUIGhostButtonColorWhite的颜色 - -#define FillButtonColorBlue [QMUICMI fillButtonColorBlue] // QMUIFillButtonColorBlue的颜色 -#define FillButtonColorRed [QMUICMI fillButtonColorRed] // QMUIFillButtonColorRed的颜色 -#define FillButtonColorGreen [QMUICMI fillButtonColorGreen] // QMUIFillButtonColorGreen的颜色 -#define FillButtonColorGray [QMUICMI fillButtonColorGray] // QMUIFillButtonColorGray的颜色 -#define FillButtonColorWhite [QMUICMI fillButtonColorWhite] // QMUIFillButtonColorWhite的颜色 - -// 输入框 -#pragma mark - TextField & TextView -#define TextFieldTintColor [QMUICMI textFieldTintColor] // 全局UITextField、UITextView的tintColor -#define TextFieldTextInsets [QMUICMI textFieldTextInsets] // QMUITextField的内边距 +#pragma mark - TextInput +#define TextFieldTextColor [QMUICMI textFieldTextColor] // QMUITextField、QMUITextView 的文字颜色 +#define TextFieldTintColor [QMUICMI textFieldTintColor] // QMUITextField、QMUITextView 的tintColor +#define TextFieldTextInsets [QMUICMI textFieldTextInsets] // QMUITextField 的内边距 +#define KeyboardAppearance [QMUICMI keyboardAppearance] +#pragma mark - UISwitch +#define SwitchOnTintColor [QMUICMI switchOnTintColor] // UISwitch 打开时的背景色(除了圆点外的其他颜色) +#define SwitchOffTintColor [QMUICMI switchOffTintColor] // UISwitch 关闭时的背景色(除了圆点外的其他颜色) +#define SwitchThumbTintColor [QMUICMI switchThumbTintColor] // UISwitch 中间的操控圆点的颜色 #pragma mark - NavigationBar +#define NavBarUsesStandardAppearanceOnly [QMUICMI navBarUsesStandardAppearanceOnly] +#define NavBarContainerClasses [QMUICMI navBarContainerClasses] #define NavBarHighlightedAlpha [QMUICMI navBarHighlightedAlpha] #define NavBarDisabledAlpha [QMUICMI navBarDisabledAlpha] #define NavBarButtonFont [QMUICMI navBarButtonFont] #define NavBarButtonFontBold [QMUICMI navBarButtonFontBold] #define NavBarBackgroundImage [QMUICMI navBarBackgroundImage] +#define NavBarRemoveBackgroundEffectAutomatically [QMUICMI navBarRemoveBackgroundEffectAutomatically] #define NavBarShadowImage [QMUICMI navBarShadowImage] +#define NavBarShadowImageColor [QMUICMI navBarShadowImageColor] #define NavBarBarTintColor [QMUICMI navBarBarTintColor] +#define NavBarStyle [QMUICMI navBarStyle] #define NavBarTintColor [QMUICMI navBarTintColor] #define NavBarTitleColor [QMUICMI navBarTitleColor] #define NavBarTitleFont [QMUICMI navBarTitleFont] @@ -116,34 +114,42 @@ #pragma mark - TabBar +#define TabBarUsesStandardAppearanceOnly [QMUICMI tabBarUsesStandardAppearanceOnly] +#define TabBarContainerClasses [QMUICMI tabBarContainerClasses] #define TabBarBackgroundImage [QMUICMI tabBarBackgroundImage] +#define TabBarRemoveBackgroundEffectAutomatically [QMUICMI tabBarRemoveBackgroundEffectAutomatically] #define TabBarBarTintColor [QMUICMI tabBarBarTintColor] #define TabBarShadowImageColor [QMUICMI tabBarShadowImageColor] -#define TabBarTintColor [QMUICMI tabBarTintColor] +#define TabBarStyle [QMUICMI tabBarStyle] +#define TabBarItemTitleFont [QMUICMI tabBarItemTitleFont] +#define TabBarItemTitleFontSelected [QMUICMI tabBarItemTitleFontSelected] #define TabBarItemTitleColor [QMUICMI tabBarItemTitleColor] #define TabBarItemTitleColorSelected [QMUICMI tabBarItemTitleColorSelected] -#define TabBarItemTitleFont [QMUICMI tabBarItemTitleFont] - +#define TabBarItemImageColor [QMUICMI tabBarItemImageColor] +#define TabBarItemImageColorSelected [QMUICMI tabBarItemImageColorSelected] #pragma mark - Toolbar +#define ToolBarUsesStandardAppearanceOnly [QMUICMI toolBarUsesStandardAppearanceOnly] +#define ToolBarContainerClasses [QMUICMI toolBarContainerClasses] #define ToolBarHighlightedAlpha [QMUICMI toolBarHighlightedAlpha] #define ToolBarDisabledAlpha [QMUICMI toolBarDisabledAlpha] #define ToolBarTintColor [QMUICMI toolBarTintColor] #define ToolBarTintColorHighlighted [QMUICMI toolBarTintColorHighlighted] #define ToolBarTintColorDisabled [QMUICMI toolBarTintColorDisabled] #define ToolBarBackgroundImage [QMUICMI toolBarBackgroundImage] +#define ToolBarRemoveBackgroundEffectAutomatically [QMUICMI toolBarRemoveBackgroundEffectAutomatically] #define ToolBarBarTintColor [QMUICMI toolBarBarTintColor] #define ToolBarShadowImageColor [QMUICMI toolBarShadowImageColor] +#define ToolBarStyle [QMUICMI toolBarStyle] #define ToolBarButtonFont [QMUICMI toolBarButtonFont] #pragma mark - SearchBar -#define SearchBarTextFieldBackground [QMUICMI searchBarTextFieldBackground] #define SearchBarTextFieldBorderColor [QMUICMI searchBarTextFieldBorderColor] -#define SearchBarBottomBorderColor [QMUICMI searchBarBottomBorderColor] -#define SearchBarBarTintColor [QMUICMI searchBarBarTintColor] +#define SearchBarTextFieldBackgroundImage [QMUICMI searchBarTextFieldBackgroundImage] +#define SearchBarBackgroundImage [QMUICMI searchBarBackgroundImage] #define SearchBarTintColor [QMUICMI searchBarTintColor] #define SearchBarTextColor [QMUICMI searchBarTextColor] #define SearchBarPlaceholderColor [QMUICMI searchBarPlaceholderColor] @@ -158,15 +164,17 @@ #define TableViewEstimatedHeightEnabled [QMUICMI tableViewEstimatedHeightEnabled] // 是否要开启全局 UITableView 的 estimatedRow(Section/Footer)Height #define TableViewBackgroundColor [QMUICMI tableViewBackgroundColor] // 普通列表的背景色 -#define TableViewGroupedBackgroundColor [QMUICMI tableViewGroupedBackgroundColor] // Grouped类型的列表的背景色 -#define TableSectionIndexColor [QMUICMI tableSectionIndexColor] // 列表右边索引条的文字颜色,iOS6及以后生效 -#define TableSectionIndexBackgroundColor [QMUICMI tableSectionIndexBackgroundColor] // 列表右边索引条的背景色,iOS7及以后生效 -#define TableSectionIndexTrackingBackgroundColor [QMUICMI tableSectionIndexTrackingBackgroundColor] // 列表右边索引条按下时的背景色,iOS6及以后生效 +#define TableSectionIndexColor [QMUICMI tableSectionIndexColor] // 列表右边索引条的文字颜色 +#define TableSectionIndexBackgroundColor [QMUICMI tableSectionIndexBackgroundColor] // 列表右边索引条的背景色 +#define TableSectionIndexTrackingBackgroundColor [QMUICMI tableSectionIndexTrackingBackgroundColor] // 列表右边索引条按下时的背景色 #define TableViewSeparatorColor [QMUICMI tableViewSeparatorColor] // 列表分隔线颜色 + +#define TableViewCellNormalHeight [QMUICMI tableViewCellNormalHeight] // QMUITableView 的默认 cell 高度 +#define TableViewCellTitleLabelColor [QMUICMI tableViewCellTitleLabelColor] // cell的title颜色 +#define TableViewCellDetailLabelColor [QMUICMI tableViewCellDetailLabelColor] // cell的detailTitle颜色 #define TableViewCellBackgroundColor [QMUICMI tableViewCellBackgroundColor] // 列表 cell 的背景色 #define TableViewCellSelectedBackgroundColor [QMUICMI tableViewCellSelectedBackgroundColor] // 列表 cell 按下时的背景色 -#define TableViewCellWarningBackgroundColor [QMUICMI tableViewCellWarningBackgroundColor] // 列表 cell 在未读状态下的背景色 -#define TableViewCellNormalHeight [QMUICMI tableViewCellNormalHeight] // QMUITableView 的默认 cell 高度 +#define TableViewCellWarningBackgroundColor [QMUICMI tableViewCellWarningBackgroundColor] // 列表 cell 在提醒状态下的背景色 #define TableViewCellDisclosureIndicatorImage [QMUICMI tableViewCellDisclosureIndicatorImage] // 列表 cell 右边的箭头图片 #define TableViewCellCheckmarkImage [QMUICMI tableViewCellCheckmarkImage] // 列表 cell 右边的打钩checkmark @@ -183,7 +191,15 @@ #define TableViewSectionFooterAccessoryMargins [QMUICMI tableViewSectionFooterAccessoryMargins] #define TableViewSectionHeaderContentInset [QMUICMI tableViewSectionHeaderContentInset] #define TableViewSectionFooterContentInset [QMUICMI tableViewSectionFooterContentInset] - +#define TableViewSectionHeaderTopPadding [QMUICMI tableViewSectionHeaderTopPadding] + +#define TableViewGroupedBackgroundColor [QMUICMI tableViewGroupedBackgroundColor] // Grouped 类型的 QMUITableView 的背景色 +#define TableViewGroupedSeparatorColor [QMUICMI tableViewGroupedSeparatorColor] // Grouped 类型的 QMUITableView 分隔线颜色 +#define TableViewGroupedCellTitleLabelColor [QMUICMI tableViewGroupedCellTitleLabelColor] // Grouped 类型的列表的 QMUITableViewCell 的标题颜色 +#define TableViewGroupedCellDetailLabelColor [QMUICMI tableViewGroupedCellDetailLabelColor] // Grouped 类型的列表的 QMUITableViewCell 的副标题颜色 +#define TableViewGroupedCellBackgroundColor [QMUICMI tableViewGroupedCellBackgroundColor] // Grouped 类型的列表的 QMUITableViewCell 的背景色 +#define TableViewGroupedCellSelectedBackgroundColor [QMUICMI tableViewGroupedCellSelectedBackgroundColor] // Grouped 类型的列表的 QMUITableViewCell 点击时的背景色 +#define TableViewGroupedCellWarningBackgroundColor [QMUICMI tableViewGroupedCellWarningBackgroundColor] // Grouped 类型的列表的 QMUITableViewCell 在提醒状态下的背景色 #define TableViewGroupedSectionHeaderFont [QMUICMI tableViewGroupedSectionHeaderFont] #define TableViewGroupedSectionFooterFont [QMUICMI tableViewGroupedSectionFooterFont] #define TableViewGroupedSectionHeaderTextColor [QMUICMI tableViewGroupedSectionHeaderTextColor] @@ -194,44 +210,65 @@ #define TableViewGroupedSectionFooterDefaultHeight [QMUICMI tableViewGroupedSectionFooterDefaultHeight] #define TableViewGroupedSectionHeaderContentInset [QMUICMI tableViewGroupedSectionHeaderContentInset] #define TableViewGroupedSectionFooterContentInset [QMUICMI tableViewGroupedSectionFooterContentInset] - -#define TableViewCellTitleLabelColor [QMUICMI tableViewCellTitleLabelColor] // cell的title颜色 -#define TableViewCellDetailLabelColor [QMUICMI tableViewCellDetailLabelColor] // cell的detailTitle颜色 +#define TableViewGroupedSectionHeaderTopPadding [QMUICMI tableViewGroupedSectionHeaderTopPadding] + +#define TableViewInsetGroupedCornerRadius [QMUICMI tableViewInsetGroupedCornerRadius] // InsetGrouped 类型的 UITableView 内 cell 的圆角值 +#define TableViewInsetGroupedHorizontalInset [QMUICMI tableViewInsetGroupedHorizontalInset] // InsetGrouped 类型的 UITableView 内的左右缩进值 +#define TableViewInsetGroupedBackgroundColor [QMUICMI tableViewInsetGroupedBackgroundColor] // InsetGrouped 类型的 UITableView 的背景色 +#define TableViewInsetGroupedSeparatorColor [QMUICMI tableViewInsetGroupedSeparatorColor] // InsetGrouped 类型的 QMUITableView 分隔线颜色 +#define TableViewInsetGroupedCellTitleLabelColor [QMUICMI tableViewInsetGroupedCellTitleLabelColor] // InsetGrouped 类型的列表的 QMUITableViewCell 的标题颜色 +#define TableViewInsetGroupedCellDetailLabelColor [QMUICMI tableViewInsetGroupedCellDetailLabelColor] // InsetGrouped 类型的列表的 QMUITableViewCell 的副标题颜色 +#define TableViewInsetGroupedCellBackgroundColor [QMUICMI tableViewInsetGroupedCellBackgroundColor] // InsetGrouped 类型的列表的 QMUITableViewCell 的背景色 +#define TableViewInsetGroupedCellSelectedBackgroundColor [QMUICMI tableViewInsetGroupedCellSelectedBackgroundColor] // InsetGrouped 类型的列表的 QMUITableViewCell 点击时的背景色 +#define TableViewInsetGroupedCellWarningBackgroundColor [QMUICMI tableViewInsetGroupedCellWarningBackgroundColor] // InsetGrouped 类型的列表的 QMUITableViewCell 在提醒状态下的背景色 +#define TableViewInsetGroupedSectionHeaderFont [QMUICMI tableViewInsetGroupedSectionHeaderFont] +#define TableViewInsetGroupedSectionFooterFont [QMUICMI tableViewInsetGroupedSectionFooterFont] +#define TableViewInsetGroupedSectionHeaderTextColor [QMUICMI tableViewInsetGroupedSectionHeaderTextColor] +#define TableViewInsetGroupedSectionFooterTextColor [QMUICMI tableViewInsetGroupedSectionFooterTextColor] +#define TableViewInsetGroupedSectionHeaderAccessoryMargins [QMUICMI tableViewInsetGroupedSectionHeaderAccessoryMargins] +#define TableViewInsetGroupedSectionFooterAccessoryMargins [QMUICMI tableViewInsetGroupedSectionFooterAccessoryMargins] +#define TableViewInsetGroupedSectionHeaderDefaultHeight [QMUICMI tableViewInsetGroupedSectionHeaderDefaultHeight] +#define TableViewInsetGroupedSectionFooterDefaultHeight [QMUICMI tableViewInsetGroupedSectionFooterDefaultHeight] +#define TableViewInsetGroupedSectionHeaderContentInset [QMUICMI tableViewInsetGroupedSectionHeaderContentInset] +#define TableViewInsetGroupedSectionFooterContentInset [QMUICMI tableViewInsetGroupedSectionFooterContentInset] +#define TableViewInsetGroupedSectionHeaderTopPadding [QMUICMI tableViewInsetGroupedSectionHeaderTopPadding] #pragma mark - UIWindowLevel #define UIWindowLevelQMUIAlertView [QMUICMI windowLevelQMUIAlertView] +#define UIWindowLevelQMUIConsole [QMUICMI windowLevelQMUIConsole] #pragma mark - QMUILog #define ShouldPrintDefaultLog [QMUICMI shouldPrintDefaultLog] #define ShouldPrintInfoLog [QMUICMI shouldPrintInfoLog] #define ShouldPrintWarnLog [QMUICMI shouldPrintWarnLog] +#define ShouldPrintQMUIWarnLogToConsole [QMUICMI shouldPrintQMUIWarnLogToConsole] // 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上 #pragma mark - QMUIBadge #define BadgeBackgroundColor [QMUICMI badgeBackgroundColor] #define BadgeTextColor [QMUICMI badgeTextColor] #define BadgeFont [QMUICMI badgeFont] #define BadgeContentEdgeInsets [QMUICMI badgeContentEdgeInsets] -#define BadgeContentEdgeInsets [QMUICMI badgeContentEdgeInsets] -#define BadgeCenterOffset [QMUICMI badgeCenterOffset] -#define BadgeCenterOffsetLandscape [QMUICMI badgeCenterOffsetLandscape] +#define BadgeOffset [QMUICMI badgeOffset] +#define BadgeOffsetLandscape [QMUICMI badgeOffsetLandscape] #define UpdatesIndicatorColor [QMUICMI updatesIndicatorColor] #define UpdatesIndicatorSize [QMUICMI updatesIndicatorSize] -#define UpdatesIndicatorCenterOffset [QMUICMI updatesIndicatorCenterOffset] -#define UpdatesIndicatorCenterOffsetLandscape [QMUICMI updatesIndicatorCenterOffsetLandscape] +#define UpdatesIndicatorOffset [QMUICMI updatesIndicatorOffset] +#define UpdatesIndicatorOffsetLandscape [QMUICMI updatesIndicatorOffsetLandscape] #pragma mark - Others #define AutomaticCustomNavigationBarTransitionStyle [QMUICMI automaticCustomNavigationBarTransitionStyle] // 界面 push/pop 时是否要自动根据两个界面的 barTintColor/backgroundImage/shadowImage 的样式差异来决定是否使用自定义的导航栏效果 #define SupportedOrientationMask [QMUICMI supportedOrientationMask] // 默认支持的横竖屏方向 -#define AutomaticallyRotateDeviceOrientation [QMUICMI automaticallyRotateDeviceOrientation] // 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕,默认为 NO -#define StatusbarStyleLightInitially [QMUICMI statusbarStyleLightInitially] // 默认的状态栏内容是否使用白色,默认为NO,也即黑色 +#define AutomaticallyRotateDeviceOrientation [QMUICMI automaticallyRotateDeviceOrientation] // 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕,默认为 NO(仅 iOS 15 及以前版本需要,iOS 16 系统会自动处理,该开关无意义)。 +#define DefaultStatusBarStyle [QMUICMI defaultStatusBarStyle] // 默认的状态栏样式,默认值为 UIStatusBarStyleDefault,也即在 iOS 12 及以前是黑色文字,iOS 13 及以后会自动根据当前 App 是否处于 Dark Mode 切换颜色。如果你希望固定为白色,请设置为 UIStatusBarStyleLightContent,固定黑色则设置为 UIStatusBarStyleDarkContent。 #define NeedsBackBarButtonItemTitle [QMUICMI needsBackBarButtonItemTitle] // 全局是否需要返回按钮的title,不需要则只显示一个返回image #define HidesBottomBarWhenPushedInitially [QMUICMI hidesBottomBarWhenPushedInitially] // QMUICommonViewController.hidesBottomBarWhenPushed 的初始值,默认为 NO,以保持与系统默认值一致,但通常建议改为 YES,因为一般只有 tabBar 首页那几个界面要求为 NO #define PreventConcurrentNavigationControllerTransitions [QMUICMI preventConcurrentNavigationControllerTransitions] // PreventConcurrentNavigationControllerTransitions : 自动保护 QMUINavigationController 在上一次 push/pop 尚未结束的时候就进行下一次 push/pop 的行为,避免产生 crash #define NavigationBarHiddenInitially [QMUICMI navigationBarHiddenInitially] // preferredNavigationBarHidden 的初始值,默认为NO -#define ShouldFixTabBarTransitionBugInIPhoneX [QMUICMI shouldFixTabBarTransitionBugInIPhoneX] // 是否需要自动修复 iOS 11 下,iPhone X 的设备在 push 界面时,tabBar 会瞬间往上跳的 bug -#define ShouldFixTabBarButtonBugForAll [QMUICMI shouldFixTabBarButtonBugForAll] // 是否要对 iOS 12.1.2 及以后的版本也修复手势返回时 tabBarButton 布局错误的 bug(issue #410),默认为 NO -#define ShouldPrintQMUIWarnLogToConsole [QMUICMI shouldPrintQMUIWarnLogToConsole] // 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上 -#define SendAnalyticsToQMUITeam [QMUICMI sendAnalyticsToQMUITeam] // 是否允许在 DEBUG 模式下上报 Bundle Identifier 和 Display Name 给 QMUI 统计用 +#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 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/QMUI/QMUIKit/QMUICore/QMUICore.h b/QMUI/QMUIKit/QMUICore/QMUICore.h index a11cc1e0..08efa51b 100644 --- a/QMUI/QMUIKit/QMUICore/QMUICore.h +++ b/QMUI/QMUIKit/QMUICore/QMUICore.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUICore.h diff --git a/QMUI/QMUIKit/QMUICore/QMUIHelper.h b/QMUI/QMUIKit/QMUICore/QMUIHelper.h index 3c57c5ea..1129501b 100644 --- a/QMUI/QMUIKit/QMUICore/QMUIHelper.h +++ b/QMUI/QMUIKit/QMUICore/QMUIHelper.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIHelper.h @@ -15,23 +15,33 @@ #import #import +#import "QMUICommonDefines.h" + +NS_ASSUME_NONNULL_BEGIN @interface QMUIHelper : NSObject -+ (instancetype _Nonnull)sharedInstance; -@end ++ (instancetype)sharedInstance; +/** + 用一个 identifier 标记某一段 block,使其对应该 identifier 只会被运行一次 + @param block 要执行的一段逻辑 + @param identifier 唯一的标记,建议在 identifier 里添加当前这段业务的特有名称,例如用于 swizzle 的可以加“swizzled”前缀,以避免与其他业务共用同一个 identifier 引发 bug + */ ++ (BOOL)executeBlock:(void (NS_NOESCAPE ^)(void))block oncePerIdentifier:(NSString *)identifier; -extern NSString *const _Nonnull QMUIResourcesMainBundleName; +/** + 将 UIViewContentMode 转为对应的 CALayerContentsGravity + */ ++ (CALayerContentsGravity)layerContentsGravityWithContentMode:(UIViewContentMode)contentMode; +@end @interface QMUIHelper (Bundle) -// QMUI专属 -+ (nullable NSBundle *)resourcesBundle; -+ (nullable UIImage *)imageWithName:(nullable NSString *)name; +/// 获取 QMUIKit.framework Images.xcassets 内的图片资源 +/// @param name 图片名 ++ (nullable UIImage *)imageWithName:(NSString *)name; -+ (nullable NSBundle *)resourcesBundleWithName:(nullable NSString *)bundleName; -+ (nullable UIImage *)imageInBundle:(nullable NSBundle *)bundle withName:(nullable NSString *)name; @end @interface QMUIHelper (SystemVersion) @@ -118,89 +128,135 @@ extern NSString *const _Nonnull QMUIResourcesMainBundleName; @interface QMUIHelper (UIGraphic) /// 获取一像素的大小 -+ (CGFloat)pixelOne; +@property(class, nonatomic, readonly) CGFloat pixelOne; /// 判断size是否超出范围 + (void)inspectContextSize:(CGSize)size; /// context是否合法 -+ (void)inspectContextIfInvalidatedInDebugMode:(CGContextRef _Nonnull)context; -+ (BOOL)inspectContextIfInvalidatedInReleaseMode:(CGContextRef _Nonnull)context; ++ (BOOL)inspectContextIfInvalidated:(CGContextRef)context; @end @interface QMUIHelper (Device) -+ (nonnull NSString *)deviceModel; +/// 如 iPhone12,5、iPad6,8 +/// @NEW_DEVICE_CHECKER +@property(class, nonatomic, readonly) NSString *deviceModel; + +/// 如 iPhone 11 Pro Max、iPad Pro (12.9 inch),如果是模拟器,会在后面带上“ Simulator”字样。 +/// @NEW_DEVICE_CHECKER +@property(class, nonatomic, readonly) NSString *deviceName; -+ (BOOL)isIPad; -+ (BOOL)isIPod; -+ (BOOL)isIPhone; -+ (BOOL)isSimulator; +@property(class, nonatomic, readonly) BOOL isIPad; +@property(class, nonatomic, readonly) BOOL isIPod; +@property(class, nonatomic, readonly) BOOL isIPhone; +@property(class, nonatomic, readonly) BOOL isSimulator; +@property(class, nonatomic, readonly) BOOL isMac; /// 带物理凹槽的刘海屏或者使用 Home Indicator 类型的设备 -+ (BOOL)isNotchedScreen; +/// @NEW_DEVICE_CHECKER +@property(class, nonatomic, readonly) BOOL isNotchedScreen; -/// 将屏幕分为普通和紧凑两种,这个方法用于判断普通屏幕 -+ (BOOL)isRegularScreen; +/// 将屏幕分为普通和紧凑两种,这个方法用于判断普通屏幕(也即大屏幕)。 +/// @note 注意,这里普通/紧凑的标准是 QMUI 自行制定的,与系统 UITraitCollection.horizontalSizeClass/verticalSizeClass 的值无关。只要是通常意义上的“大屏幕手机”(例如 Plus 系列)都会被视为 Regular Screen。 +/// @NEW_DEVICE_CHECKER +@property(class, nonatomic, readonly) BOOL isRegularScreen; -/// iPhone XS Max -+ (BOOL)is65InchScreen; +/// iPhone 16 Pro Max +@property(class, nonatomic, readonly) BOOL is69InchScreen; -/// iPhone XR -+ (BOOL)is61InchScreen; +/// iPhone 14 Pro Max +@property(class, nonatomic, readonly) BOOL is67InchScreenAndiPhone14Later; -/// iPhone X/XS -+ (BOOL)is58InchScreen; +/// iPhone 14 Plus / 13 Pro Max / 12 Pro Max +@property(class, nonatomic, readonly) BOOL is67InchScreen; + +/// iPhone XS Max / 11 Pro Max +@property(class, nonatomic, readonly) BOOL is65InchScreen; + +/// iPhone 16 Pro +@property(class, nonatomic, readonly) BOOL is63InchScreen; + +/// iPhone 12 / 12 Pro +@property(class, nonatomic, readonly) BOOL is61InchScreenAndiPhone12Later; + +/// iPhone 14 Pro / 15 Pro +@property(class, nonatomic, readonly) BOOL is61InchScreenAndiPhone14ProLater; + +/// iPhone XR / 11 +@property(class, nonatomic, readonly) BOOL is61InchScreen; + +/// iPhone X / XS / 11Pro +@property(class, nonatomic, readonly) BOOL is58InchScreen; /// iPhone 8 Plus -+ (BOOL)is55InchScreen; +@property(class, nonatomic, readonly) BOOL is55InchScreen; + +/// iPhone 12 mini +@property(class, nonatomic, readonly) BOOL is54InchScreen; /// iPhone 8 -+ (BOOL)is47InchScreen; +@property(class, nonatomic, readonly) BOOL is47InchScreen; /// iPhone 5 -+ (BOOL)is40InchScreen; +@property(class, nonatomic, readonly) BOOL is40InchScreen; /// iPhone 4 -+ (BOOL)is35InchScreen; - -+ (CGSize)screenSizeFor65Inch; -+ (CGSize)screenSizeFor61Inch; -+ (CGSize)screenSizeFor58Inch; -+ (CGSize)screenSizeFor55Inch; -+ (CGSize)screenSizeFor47Inch; -+ (CGSize)screenSizeFor40Inch; -+ (CGSize)screenSizeFor35Inch; - -+ (CGFloat)preferredLayoutAsSimilarScreenWidthForIPad; - -// 用于获取 isNotchedScreen 设备的 insets,注意对于 iPad Pro 11-inch 这种无刘海凹槽但却有使用 Home Indicator 的设备,它的 top 返回0,bottom 返回 safeAreaInsets.bottom 的值 -+ (UIEdgeInsets)safeAreaInsetsForDeviceWithNotch; +@property(class, nonatomic, readonly) BOOL is35InchScreen; + +@property(class, nonatomic, readonly) CGSize screenSizeFor69Inch; +@property(class, nonatomic, readonly) CGSize screenSizeFor67InchAndiPhone14Later; +@property(class, nonatomic, readonly) CGSize screenSizeFor67Inch; +@property(class, nonatomic, readonly) CGSize screenSizeFor65Inch; +@property(class, nonatomic, readonly) CGSize screenSizeFor63Inch; +@property(class, nonatomic, readonly) CGSize screenSizeFor61InchAndiPhone14ProLater; +@property(class, nonatomic, readonly) CGSize screenSizeFor61InchAndiPhone12Later; +@property(class, nonatomic, readonly) CGSize screenSizeFor61Inch; +@property(class, nonatomic, readonly) CGSize screenSizeFor58Inch; +@property(class, nonatomic, readonly) CGSize screenSizeFor55Inch; +@property(class, nonatomic, readonly) CGSize screenSizeFor54Inch; +@property(class, nonatomic, readonly) CGSize screenSizeFor47Inch; +@property(class, nonatomic, readonly) CGSize screenSizeFor40Inch; +@property(class, nonatomic, readonly) CGSize screenSizeFor35Inch; + +@property(class, nonatomic, readonly) CGFloat preferredLayoutAsSimilarScreenWidthForIPad; + +/// 用于获取 isNotchedScreen 设备的 insets,注意对于无 Home 键的新款 iPad 而言,它不一定有物理凹槽,但因为使用了 Home Indicator,所以它的 safeAreaInsets 也是非0。 +/// @NEW_DEVICE_CHECKER +@property(class, nonatomic, readonly) UIEdgeInsets safeAreaInsetsForDeviceWithNotch; /// 判断当前设备是否高性能设备,只会判断一次,以后都直接读取结果,所以没有性能问题 -+ (BOOL)isHighPerformanceDevice; +@property(class, nonatomic, readonly) BOOL isHighPerformanceDevice; /// 系统设置里是否开启了“放大显示-试图-放大”,支持放大模式的 iPhone 设备可在官方文档中查询 https://support.apple.com/zh-cn/guide/iphone/iphd6804774e/ios -+ (BOOL)isZoomedMode; +/// @NEW_DEVICE_CHECKER +@property(class, nonatomic, readonly) BOOL isZoomedMode; -@end +/// 当前设备是否拥有灵动岛 +/// @NEW_DEVICE_CHECKER +@property(class, nonatomic, readonly) BOOL isDynamicIslandDevice; -@interface QMUIHelper (UIApplication) +/** + 在 iPad 分屏模式下可获得实际运行区域的窗口大小,如需适配 iPad 分屏,建议用这个方法来代替 [UIScreen mainScreen].bounds.size + @return 应用运行的窗口大小 + */ +@property(class, nonatomic, readonly) CGSize applicationSize; /** - * 更改状态栏内容颜色为深色 - * - * @warning 需在项目的 Info.plist 文件内设置字段 “View controller-based status bar appearance” 的值为 NO 才能生效,如果不设置,或者值为 YES,则请通过系统的 - UIViewController preferredStatusBarStyle 方法来修改 + 静态的状态栏高度,在状态栏不可见时也会根据机型返回状态栏的固定高度 + @NEW_DEVICE_CHECKER */ -+ (void)renderStatusBarStyleDark DEPRECATED_ATTRIBUTE; +@property(class, nonatomic, readonly) CGFloat statusBarHeightConstant; /** - * 更改状态栏内容颜色为浅色 - * - * @warning 需在项目的 Info.plist 文件内设置字段 “View controller-based status bar appearance” 的值为 NO 才能生效,如果不设置,或者值为 YES,则请通过系统的 - UIViewController preferredStatusBarStyle 方法来修改 + 静态的导航栏高度,在导航栏不可见时也会根据机型返回导航栏的固定高度 */ -+ (void)renderStatusBarStyleLight DEPRECATED_ATTRIBUTE; +@property(class, nonatomic, readonly) CGFloat navigationBarMaxYConstant; + +@end + +@interface QMUIHelper (UIApplication) /** * 把App的主要window置灰,用于浮层弹出时,请注意要在适当时机调用`resetDimmedApplicationWindow`恢复到正常状态 @@ -212,16 +268,36 @@ extern NSString *const _Nonnull QMUIResourcesMainBundleName; */ + (void)resetDimmedApplicationWindow; +/** + 在非 UIApplicationStateActive 的时机去设置 UIAppearance 可能引发第三方输入法 crash,因此提供这个方法判断当前是否可以更新 UIAppearance。 + 详情请见 https://github.com/Tencent/QMUI_iOS/issues/1281 + */ +@property(class, nonatomic, assign, readonly) BOOL canUpdateAppearance; + @end @interface QMUIHelper (Animation) /** - 在 animationBlock 里的操作完成之后会调用 completionBlock,常用于一些不提供 completionBlock 的系统动画操作,例如 [UINavigationController pushViewController:animated:YES] 的场景,注意 UIScrollView 系列的滚动无法使用这个方法。 + 在 animationBlock 里的操作完成之后会调用 completionBlock,常用于一些不提供 completionBlock 的系统动画操作。 @param animationBlock 要进行的带动画的操作 @param completionBlock 操作完成后的回调 + @note 注意 UIScrollView 系列的滚动无法使用这个方法。 */ + (void)executeAnimationBlock:(nonnull __attribute__((noescape)) void (^)(void))animationBlock completionBlock:(nullable __attribute__((noescape)) void (^)(void))completionBlock; @end + +@interface QMUIHelper (Text) + +/** + 该方法计算一个 baselineOffset,使得指定字体的文本在指定高度里能达到视觉上的垂直居中(系统默认是底对齐)。 + @param height 单行文本占据的高度,通常可传入文本的 lineHeight 或者 UILabel 的 height。 + @param font 当前文本的字体。 + @return 可使文本垂直居中的 baselineOffset 偏移值,正值往上,负值往下。注意如果某段 NSAttributedString 通过 NSParagraphStyle 指定了行高,则负值的 baselineOffset 对其无效。 + */ ++ (CGFloat)baselineOffsetWhenVerticalAlignCenterInHeight:(CGFloat)height withFont:(UIFont *)font; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUICore/QMUIHelper.m b/QMUI/QMUIKit/QMUICore/QMUIHelper.m index 7b6a2298..c313d23b 100644 --- a/QMUI/QMUIKit/QMUICore/QMUIHelper.m +++ b/QMUI/QMUIKit/QMUICore/QMUIHelper.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIHelper.m @@ -20,54 +20,44 @@ #import "NSString+QMUI.h" #import "UIInterface+QMUI.h" #import "NSObject+QMUI.h" +#import "NSArray+QMUI.h" #import #import #import -NSString *const QMUIResourcesMainBundleName = @"QMUIResources.bundle"; +NSString *const kQMUIResourcesBundleName = @"QMUIResources"; -@implementation QMUIHelper (Bundle) +@interface _QMUIPortraitViewController : UIViewController +@end -+ (NSBundle *)resourcesBundle { - return [QMUIHelper resourcesBundleWithName:QMUIResourcesMainBundleName]; -} +@implementation _QMUIPortraitViewController -+ (NSBundle *)resourcesBundleWithName:(NSString *)bundleName { - NSBundle *bundle = [NSBundle bundleWithPath: [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:bundleName]]; - if (!bundle) { - // 动态framework的bundle资源是打包在framework里面的,所以无法通过mainBundle拿到资源,只能通过其他方法来获取bundle资源。 - NSBundle *frameworkBundle = [NSBundle bundleForClass:[self class]]; - NSDictionary *bundleData = [self parseBundleName:bundleName]; - if (bundleData) { - bundle = [NSBundle bundleWithPath:[frameworkBundle pathForResource:[bundleData objectForKey:@"name"] ofType:[bundleData objectForKey:@"type"]]]; - } - } - return bundle; +- (BOOL)shouldAutorotate { + return NO; } -+ (UIImage *)imageWithName:(NSString *)name { - NSBundle *bundle = [QMUIHelper resourcesBundle]; - return [QMUIHelper imageInBundle:bundle withName:name]; +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + return UIInterfaceOrientationMaskPortrait; } -+ (UIImage *)imageInBundle:(NSBundle *)bundle withName:(NSString *)name { - if (bundle && name) { - if ([UIImage respondsToSelector:@selector(imageNamed:inBundle:compatibleWithTraitCollection:)]) { - return [UIImage imageNamed:name inBundle:bundle compatibleWithTraitCollection:nil]; - } else { - NSString *imagePath = [[bundle resourcePath] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.png", name]]; - return [UIImage imageWithContentsOfFile:imagePath]; - } - } - return nil; -} +@end + +@interface QMUIHelper () + +@property(nonatomic, assign) BOOL shouldPreventAppearanceUpdating; +@end + +@implementation QMUIHelper (Bundle) -+ (NSDictionary *)parseBundleName:(NSString *)bundleName { - NSArray *bundleData = [bundleName componentsSeparatedByString:@"."]; - if (bundleData.count == 2) { - return @{@"name":bundleData[0], @"type":bundleData[1]}; ++ (UIImage *)imageWithName:(NSString *)name { + static NSBundle *resourceBundle = nil; + if (!resourceBundle) { + NSBundle *mainBundle = [NSBundle bundleForClass:self]; + NSString *resourcePath = [mainBundle pathForResource:kQMUIResourcesBundleName ofType:@"bundle"]; + resourceBundle = [NSBundle bundleWithPath:resourcePath] ?: mainBundle; } - return nil; + UIImage *image = [UIImage imageNamed:name inBundle:resourceBundle compatibleWithTraitCollection:nil]; + return image; } @end @@ -78,7 +68,7 @@ @implementation QMUIHelper (DynamicType) + (NSNumber *)preferredContentSizeLevel { NSNumber *index = nil; if ([UIApplication instancesRespondToSelector:@selector(preferredContentSizeCategory)]) { - NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; + NSString *contentSizeCategory = UIApplication.sharedApplication.preferredContentSizeCategory; if ([contentSizeCategory isEqualToString:UIContentSizeCategoryExtraSmall]) { index = [NSNumber numberWithInt:0]; } else if ([contentSizeCategory isEqualToString:UIContentSizeCategorySmall]) { @@ -155,10 +145,12 @@ + (CGFloat)keyboardHeightWithNotification:(NSNotification *)notification { + (CGFloat)keyboardHeightWithNotification:(nullable NSNotification *)notification inView:(nullable UIView *)view { CGRect keyboardRect = [self keyboardRectWithNotification:notification]; - if (!view) { - return CGRectGetHeight(keyboardRect); + // iOS 13 分屏键盘 x 不是 0,不知道是系统 BUG 还是故意这样,先这样保护,再观察一下后面的 beta 版本 + if (IS_SPLIT_SCREEN_IPAD && keyboardRect.origin.x > 0) { + keyboardRect.origin.x = 0; } - CGRect keyboardRectInView = [view convertRect:keyboardRect fromView:view.window]; + if (!view) { return CGRectGetHeight(keyboardRect); } + CGRect keyboardRectInView = [view convertRect:keyboardRect fromCoordinateSpace:UIScreen.mainScreen.coordinateSpace]; CGRect keyboardVisibleRectInView = CGRectIntersection(view.bounds, keyboardRectInView); CGFloat resultHeight = CGRectIsValidated(keyboardVisibleRectInView) ? CGRectGetHeight(keyboardVisibleRectInView) : 0; return resultHeight; @@ -204,8 +196,7 @@ + (void)setAudioSessionCategory:(nullable NSString *)category { category != AVAudioSessionCategorySoloAmbient && category != AVAudioSessionCategoryPlayback && category != AVAudioSessionCategoryRecord && - category != AVAudioSessionCategoryPlayAndRecord && - category != AVAudioSessionCategoryAudioProcessing) + category != AVAudioSessionCategoryPlayAndRecord) { return; } @@ -213,28 +204,6 @@ + (void)setAudioSessionCategory:(nullable NSString *)category { [[AVAudioSession sharedInstance] setCategory:category error:nil]; } -+ (UInt32)categoryForLowVersionWithCategory:(NSString *)category { - if ([category isEqualToString:AVAudioSessionCategoryAmbient]) { - return kAudioSessionCategory_AmbientSound; - } - if ([category isEqualToString:AVAudioSessionCategorySoloAmbient]) { - return kAudioSessionCategory_SoloAmbientSound; - } - if ([category isEqualToString:AVAudioSessionCategoryPlayback]) { - return kAudioSessionCategory_MediaPlayback; - } - if ([category isEqualToString:AVAudioSessionCategoryRecord]) { - return kAudioSessionCategory_RecordAudio; - } - if ([category isEqualToString:AVAudioSessionCategoryPlayAndRecord]) { - return kAudioSessionCategory_PlayAndRecord; - } - if ([category isEqualToString:AVAudioSessionCategoryAudioProcessing]) { - return kAudioSessionCategory_AudioProcessing; - } - return kAudioSessionCategory_AmbientSound; -} - @end @@ -250,22 +219,17 @@ + (CGFloat)pixelOne { + (void)inspectContextSize:(CGSize)size { if (!CGSizeIsValidated(size)) { - NSAssert(NO, @"QMUI CGPostError, %@:%d %s, 非法的size:%@\n%@", [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__, NSStringFromCGSize(size), [NSThread callStackSymbols]); + QMUIAssert(NO, @"QMUIHelper (UIGraphic)", @"QMUI CGPostError, %@:%d %s, 非法的size:%@\n%@", [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__, NSStringFromCGSize(size), [NSThread callStackSymbols]); } } -+ (void)inspectContextIfInvalidatedInDebugMode:(CGContextRef)context { ++ (BOOL)inspectContextIfInvalidated:(CGContextRef)context { if (!context) { - // crash了就找zhoon或者molice - NSAssert(NO, @"QMUI CGPostError, %@:%d %s, 非法的context:%@\n%@", [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__, context, [NSThread callStackSymbols]); - } -} - -+ (BOOL)inspectContextIfInvalidatedInReleaseMode:(CGContextRef)context { - if (context) { - return YES; + // crash 了就找 molice + QMUIAssert(NO, @"QMUIHelper (UIGraphic)", @"QMUI CGPostError, %@:%d %s, 非法的context:%@\n%@", [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__, context, [NSThread callStackSymbols]); + return NO; } - return NO; + return YES; } @end @@ -279,17 +243,252 @@ + (NSString *)deviceModel { return [NSString stringWithFormat:@"%s", getenv("SIMULATOR_MODEL_IDENTIFIER")]; } - // See https://www.theiphonewiki.com/wiki/Models for identifiers - struct utsname systemInfo; - uname(&systemInfo); - return [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; + // See https://gist.github.com/adamawolf/3048717 for identifiers + static dispatch_once_t onceToken; + static NSString *model; + dispatch_once(&onceToken, ^{ + struct utsname systemInfo; + uname(&systemInfo); + model = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; + }); + return model; +} + ++ (NSString *)deviceName { + static dispatch_once_t onceToken; + static NSString *name; + dispatch_once(&onceToken, ^{ + NSString *model = [self deviceModel]; + if (!model) { + name = @"Unknown Device"; + return; + } + + NSDictionary *dict = @{ + // See https://gist.github.com/adamawolf/3048717 + @"iPhone1,1" : @"iPhone 1G", + @"iPhone1,2" : @"iPhone 3G", + @"iPhone2,1" : @"iPhone 3GS", + @"iPhone3,1" : @"iPhone 4 (GSM)", + @"iPhone3,2" : @"iPhone 4", + @"iPhone3,3" : @"iPhone 4 (CDMA)", + @"iPhone4,1" : @"iPhone 4S", + @"iPhone5,1" : @"iPhone 5", + @"iPhone5,2" : @"iPhone 5", + @"iPhone5,3" : @"iPhone 5c", + @"iPhone5,4" : @"iPhone 5c", + @"iPhone6,1" : @"iPhone 5s", + @"iPhone6,2" : @"iPhone 5s", + @"iPhone7,1" : @"iPhone 6 Plus", + @"iPhone7,2" : @"iPhone 6", + @"iPhone8,1" : @"iPhone 6s", + @"iPhone8,2" : @"iPhone 6s Plus", + @"iPhone8,4" : @"iPhone SE", + @"iPhone9,1" : @"iPhone 7", + @"iPhone9,2" : @"iPhone 7 Plus", + @"iPhone9,3" : @"iPhone 7", + @"iPhone9,4" : @"iPhone 7 Plus", + @"iPhone10,1" : @"iPhone 8", + @"iPhone10,2" : @"iPhone 8 Plus", + @"iPhone10,3" : @"iPhone X", + @"iPhone10,4" : @"iPhone 8", + @"iPhone10,5" : @"iPhone 8 Plus", + @"iPhone10,6" : @"iPhone X", + @"iPhone11,2" : @"iPhone XS", + @"iPhone11,4" : @"iPhone XS Max", + @"iPhone11,6" : @"iPhone XS Max CN", + @"iPhone11,8" : @"iPhone XR", + @"iPhone12,1" : @"iPhone 11", + @"iPhone12,3" : @"iPhone 11 Pro", + @"iPhone12,5" : @"iPhone 11 Pro Max", + @"iPhone12,8" : @"iPhone SE (2nd generation)", + @"iPhone13,1" : @"iPhone 12 mini", + @"iPhone13,2" : @"iPhone 12", + @"iPhone13,3" : @"iPhone 12 Pro", + @"iPhone13,4" : @"iPhone 12 Pro Max", + @"iPhone14,4" : @"iPhone 13 mini", + @"iPhone14,5" : @"iPhone 13", + @"iPhone14,2" : @"iPhone 13 Pro", + @"iPhone14,3" : @"iPhone 13 Pro Max", + @"iPhone14,7" : @"iPhone 14", + @"iPhone14,8" : @"iPhone 14 Plus", + @"iPhone15,2" : @"iPhone 14 Pro", + @"iPhone15,3" : @"iPhone 14 Pro Max", + @"iPhone15,4" : @"iPhone 15", + @"iPhone15,5" : @"iPhone 15 Plus", + @"iPhone16,1" : @"iPhone 15 Pro", + @"iPhone16,2" : @"iPhone 15 Pro Max", + @"iPhone17,1" : @"iPhone 16 Pro", + @"iPhone17,2" : @"iPhone 16 Pro Max", + @"iPhone17,3" : @"iPhone 16", + @"iPhone17,4" : @"iPhone 16 Plus", + + @"iPad1,1" : @"iPad 1", + @"iPad2,1" : @"iPad 2 (WiFi)", + @"iPad2,2" : @"iPad 2 (GSM)", + @"iPad2,3" : @"iPad 2 (CDMA)", + @"iPad2,4" : @"iPad 2", + @"iPad2,5" : @"iPad mini 1", + @"iPad2,6" : @"iPad mini 1", + @"iPad2,7" : @"iPad mini 1", + @"iPad3,1" : @"iPad 3 (WiFi)", + @"iPad3,2" : @"iPad 3 (4G)", + @"iPad3,3" : @"iPad 3 (4G)", + @"iPad3,4" : @"iPad 4", + @"iPad3,5" : @"iPad 4", + @"iPad3,6" : @"iPad 4", + @"iPad4,1" : @"iPad Air", + @"iPad4,2" : @"iPad Air", + @"iPad4,3" : @"iPad Air", + @"iPad4,4" : @"iPad mini 2", + @"iPad4,5" : @"iPad mini 2", + @"iPad4,6" : @"iPad mini 2", + @"iPad4,7" : @"iPad mini 3", + @"iPad4,8" : @"iPad mini 3", + @"iPad4,9" : @"iPad mini 3", + @"iPad5,1" : @"iPad mini 4", + @"iPad5,2" : @"iPad mini 4", + @"iPad5,3" : @"iPad Air 2", + @"iPad5,4" : @"iPad Air 2", + @"iPad6,3" : @"iPad Pro (9.7 inch)", + @"iPad6,4" : @"iPad Pro (9.7 inch)", + @"iPad6,7" : @"iPad Pro (12.9 inch)", + @"iPad6,8" : @"iPad Pro (12.9 inch)", + @"iPad6,11": @"iPad 5 (WiFi)", + @"iPad6,12": @"iPad 5 (Cellular)", + @"iPad7,1" : @"iPad Pro (12.9 inch, 2nd generation)", + @"iPad7,2" : @"iPad Pro (12.9 inch, 2nd generation)", + @"iPad7,3" : @"iPad Pro (10.5 inch)", + @"iPad7,4" : @"iPad Pro (10.5 inch)", + @"iPad7,5" : @"iPad 6 (WiFi)", + @"iPad7,6" : @"iPad 6 (Cellular)", + @"iPad7,11": @"iPad 7 (WiFi)", + @"iPad7,12": @"iPad 7 (Cellular)", + @"iPad8,1" : @"iPad Pro (11 inch)", + @"iPad8,2" : @"iPad Pro (11 inch)", + @"iPad8,3" : @"iPad Pro (11 inch)", + @"iPad8,4" : @"iPad Pro (11 inch)", + @"iPad8,5" : @"iPad Pro (12.9 inch, 3rd generation)", + @"iPad8,6" : @"iPad Pro (12.9 inch, 3rd generation)", + @"iPad8,7" : @"iPad Pro (12.9 inch, 3rd generation)", + @"iPad8,8" : @"iPad Pro (12.9 inch, 3rd generation)", + @"iPad8,9" : @"iPad Pro (11 inch, 2nd generation)", + @"iPad8,10" : @"iPad Pro (11 inch, 2nd generation)", + @"iPad8,11" : @"iPad Pro (12.9 inch, 4th generation)", + @"iPad8,12" : @"iPad Pro (12.9 inch, 4th generation)", + @"iPad11,1" : @"iPad mini (5th generation)", + @"iPad11,2" : @"iPad mini (5th generation)", + @"iPad11,3" : @"iPad Air (3rd generation)", + @"iPad11,4" : @"iPad Air (3rd generation)", + @"iPad11,6" : @"iPad (WiFi)", + @"iPad11,7" : @"iPad (Cellular)", + @"iPad13,1" : @"iPad Air (4th generation)", + @"iPad13,2" : @"iPad Air (4th generation)", + @"iPad13,4" : @"iPad Pro (11 inch, 3rd generation)", + @"iPad13,5" : @"iPad Pro (11 inch, 3rd generation)", + @"iPad13,6" : @"iPad Pro (11 inch, 3rd generation)", + @"iPad13,7" : @"iPad Pro (11 inch, 3rd generation)", + @"iPad13,8" : @"iPad Pro (12.9 inch, 5th generation)", + @"iPad13,9" : @"iPad Pro (12.9 inch, 5th generation)", + @"iPad13,10" : @"iPad Pro (12.9 inch, 5th generation)", + @"iPad13,11" : @"iPad Pro (12.9 inch, 5th generation)", + @"iPad14,1" : @"iPad mini (6th generation)", + @"iPad14,2" : @"iPad mini (6th generation)", + @"iPad14,3" : @"iPad Pro 11 inch 4th Gen", + @"iPad14,4" : @"iPad Pro 11 inch 4th Gen", + @"iPad14,5" : @"iPad Pro 12.9 inch 6th Gen", + @"iPad14,6" : @"iPad Pro 12.9 inch 6th Gen", + @"iPad14,8" : @"iPad Air 6th Gen", + @"iPad14,9" : @"iPad Air 6th Gen", + @"iPad14,10" : @"iPad Air 7th Gen", + @"iPad14,11" : @"iPad Air 7th Gen", + @"iPad16,3" : @"iPad Pro 11 inch 5th Gen", + @"iPad16,4" : @"iPad Pro 11 inch 5th Gen", + @"iPad16,5" : @"iPad Pro 12.9 inch 7th Gen", + @"iPad16,6" : @"iPad Pro 12.9 inch 7th Gen", + + @"iPod1,1" : @"iPod touch 1", + @"iPod2,1" : @"iPod touch 2", + @"iPod3,1" : @"iPod touch 3", + @"iPod4,1" : @"iPod touch 4", + @"iPod5,1" : @"iPod touch 5", + @"iPod7,1" : @"iPod touch 6", + @"iPod9,1" : @"iPod touch 7", + + @"i386" : @"Simulator x86", + @"x86_64" : @"Simulator x64", + + @"Watch1,1" : @"Apple Watch 38mm", + @"Watch1,2" : @"Apple Watch 42mm", + @"Watch2,3" : @"Apple Watch Series 2 38mm", + @"Watch2,4" : @"Apple Watch Series 2 42mm", + @"Watch2,6" : @"Apple Watch Series 1 38mm", + @"Watch2,7" : @"Apple Watch Series 1 42mm", + @"Watch3,1" : @"Apple Watch Series 3 38mm", + @"Watch3,2" : @"Apple Watch Series 3 42mm", + @"Watch3,3" : @"Apple Watch Series 3 38mm (LTE)", + @"Watch3,4" : @"Apple Watch Series 3 42mm (LTE)", + @"Watch4,1" : @"Apple Watch Series 4 40mm", + @"Watch4,2" : @"Apple Watch Series 4 44mm", + @"Watch4,3" : @"Apple Watch Series 4 40mm (LTE)", + @"Watch4,4" : @"Apple Watch Series 4 44mm (LTE)", + @"Watch5,1" : @"Apple Watch Series 5 40mm", + @"Watch5,2" : @"Apple Watch Series 5 44mm", + @"Watch5,3" : @"Apple Watch Series 5 40mm (LTE)", + @"Watch5,4" : @"Apple Watch Series 5 44mm (LTE)", + @"Watch5,9" : @"Apple Watch SE 40mm", + @"Watch5,10" : @"Apple Watch SE 44mm", + @"Watch5,11" : @"Apple Watch SE 40mm", + @"Watch5,12" : @"Apple Watch SE 44mm", + @"Watch6,1" : @"Apple Watch Series 6 40mm", + @"Watch6,2" : @"Apple Watch Series 6 44mm", + @"Watch6,3" : @"Apple Watch Series 6 40mm", + @"Watch6,4" : @"Apple Watch Series 6 44mm", + @"Watch6,6" : @"Apple Watch Series 7 41mm case (GPS)", + @"Watch6,7" : @"Apple Watch Series 7 45mm case (GPS)", + @"Watch6,8" : @"Apple Watch Series 7 41mm case (GPS+Cellular)", + @"Watch6,9" : @"Apple Watch Series 7 45mm case (GPS+Cellular)", + @"Watch6,10" : @"Apple Watch SE 40mm case (GPS)", + @"Watch6,11" : @"Apple Watch SE 44mm case (GPS)", + @"Watch6,12" : @"Apple Watch SE 40mm case (GPS+Cellular)", + @"Watch6,13" : @"Apple Watch SE 44mm case (GPS+Cellular)", + @"Watch6,14" : @"Apple Watch Series 8 41mm case (GPS)", + @"Watch6,15" : @"Apple Watch Series 8 45mm case (GPS)", + @"Watch6,16" : @"Apple Watch Series 8 41mm case (GPS+Cellular)", + @"Watch6,17" : @"Apple Watch Series 8 45mm case (GPS+Cellular)", + @"Watch6,18" : @"Apple Watch Ultra", + @"Watch7,1" : @"Apple Watch Series 9 41mm case (GPS)", + @"Watch7,2" : @"Apple Watch Series 9 45mm case (GPS)", + @"Watch7,3" : @"Apple Watch Series 9 41mm case (GPS+Cellular)", + @"Watch7,4" : @"Apple Watch Series 9 45mm case (GPS+Cellular)", + @"Watch7,5" : @"Apple Watch Ultra 2", + + @"AudioAccessory1,1" : @"HomePod", + @"AudioAccessory1,2" : @"HomePod", + @"AudioAccessory5,1" : @"HomePod mini", + + @"AirPods1,1" : @"AirPods (1st generation)", + @"AirPods2,1" : @"AirPods (2nd generation)", + @"iProd8,1" : @"AirPods Pro", + + @"AppleTV2,1" : @"Apple TV 2", + @"AppleTV3,1" : @"Apple TV 3", + @"AppleTV3,2" : @"Apple TV 3", + @"AppleTV5,3" : @"Apple TV 4", + @"AppleTV6,2" : @"Apple TV 4K", + }; + name = dict[model]; + if (!name) name = model; + if (IS_SIMULATOR) name = [name stringByAppendingString:@" Simulator"]; + }); + return name; } static NSInteger isIPad = -1; + (BOOL)isIPad { if (isIPad < 0) { // [[[UIDevice currentDevice] model] isEqualToString:@"iPad"] 无法判断模拟器 iPad,所以改为以下方式 - isIPad = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? 1 : 0; + isIPad = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad ? 1 : 0; } return isIPad > 0; } @@ -324,64 +523,121 @@ + (BOOL)isSimulator { return isSimulator > 0; } + ++ (BOOL)isMac { + if (@available(iOS 14.0, *)) { + return [NSProcessInfo processInfo].isiOSAppOnMac || [NSProcessInfo processInfo].isMacCatalystApp; + } + return [NSProcessInfo processInfo].isMacCatalystApp; +} + static NSInteger isNotchedScreen = -1; + (BOOL)isNotchedScreen { - if (@available(iOS 11, *)) { - if (isNotchedScreen < 0) { - if (@available(iOS 12.0, *)) { - /* - 检测方式解释/测试要点: - 1. iOS 11 与 iOS 12 可能行为不同,所以要分别测试。 - 2. 与触发 [QMUIHelper isNotchedScreen] 方法时的进程有关,例如 https://github.com/Tencent/QMUI_iOS/issues/482#issuecomment-456051738 里提到的 [NSObject performSelectorOnMainThread:withObject:waitUntilDone:NO] 就会导致较多的异常。 - 3. iOS 12 下,在非第2点里提到的情况下,iPhone、iPad 均可通过 UIScreen -_peripheryInsets 方法的返回值区分,但如果满足了第2点,则 iPad 无法使用这个方法,这种情况下要依赖第4点。 - 4. iOS 12 下,不管是否满足第2点,不管是什么设备类型,均可以通过一个满屏的 UIWindow 的 rootViewController.view.frame.origin.y 的值来区分,如果是非全面屏,这个值必定为20,如果是全面屏,则可能是24或44等不同的值。但由于创建 UIWindow、UIViewController 等均属于较大消耗,所以只在前面的步骤无法区分的情况下才会使用第4点。 - 5. 对于第4点,经测试与当前设备的方向、是否有勾选 project 里的 General - Hide status bar、当前是否处于来电模式的状态栏这些都没关系。 - */ - SEL peripheryInsetsSelector = NSSelectorFromString([NSString stringWithFormat:@"_%@%@", @"periphery", @"Insets"]); - UIEdgeInsets peripheryInsets = UIEdgeInsetsZero; - [[UIScreen mainScreen] qmui_performSelector:peripheryInsetsSelector withPrimitiveReturnValue:&peripheryInsets]; - if (peripheryInsets.bottom <= 0) { - UIWindow *window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; - peripheryInsets = window.safeAreaInsets; - if (peripheryInsets.bottom <= 0) { - UIViewController *viewController = [UIViewController new]; - [viewController loadViewIfNeeded]; - window.rootViewController = viewController; - if (CGRectGetMinY(viewController.view.frame) > 20) { - peripheryInsets.bottom = 1; - } - } + if (isNotchedScreen < 0) { + /* + 检测方式解释/测试要点: + 1. iOS 11 与 iOS 12 可能行为不同,所以要分别测试。 + 2. 与触发 [QMUIHelper isNotchedScreen] 方法时的进程有关,例如 https://github.com/Tencent/QMUI_iOS/issues/482#issuecomment-456051738 里提到的 [NSObject performSelectorOnMainThread:withObject:waitUntilDone:NO] 就会导致较多的异常。 + 3. iOS 12 下,在非第2点里提到的情况下,iPhone、iPad 均可通过 UIScreen -_peripheryInsets 方法的返回值区分,但如果满足了第2点,则 iPad 无法使用这个方法,这种情况下要依赖第4点。 + 4. iOS 12 下,不管是否满足第2点,不管是什么设备类型,均可以通过一个满屏的 UIWindow 的 rootViewController.view.frame.origin.y 的值来区分,如果是非全面屏,这个值必定为20,如果是全面屏,则可能是24或44等不同的值。但由于创建 UIWindow、UIViewController 等均属于较大消耗,所以只在前面的步骤无法区分的情况下才会使用第4点。 + 5. 对于第4点,经测试与当前设备的方向、是否有勾选 project 里的 General - Hide status bar、当前是否处于来电模式的状态栏这些都没关系。 + */ + SEL peripheryInsetsSelector = NSSelectorFromString([NSString stringWithFormat:@"_%@%@", @"periphery", @"Insets"]); + UIEdgeInsets peripheryInsets = UIEdgeInsetsZero; + [[UIScreen mainScreen] qmui_performSelector:peripheryInsetsSelector withPrimitiveReturnValue:&peripheryInsets]; + if (peripheryInsets.bottom <= 0) { + UIWindow *window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; + peripheryInsets = window.safeAreaInsets; + if (peripheryInsets.bottom <= 0) { + // 使用一个强制竖屏的 rootViewController,避免一个仅支持竖屏的 App 在横屏启动时会受这里创建的 window 的影响,导致状态栏、safeAreaInsets 等错乱 + // https://github.com/Tencent/QMUI_iOS/issues/1263 + _QMUIPortraitViewController *viewController = [_QMUIPortraitViewController new]; + window.rootViewController = viewController; + if (CGRectGetMinY(viewController.view.frame) > 20) { + peripheryInsets.bottom = 1; } - isNotchedScreen = peripheryInsets.bottom > 0 ? 1 : 0; - } else { - isNotchedScreen = [QMUIHelper is58InchScreen] ? 1 : 0; } } - } else { - isNotchedScreen = 0; + isNotchedScreen = peripheryInsets.bottom > 0 ? 1 : 0; } - return isNotchedScreen > 0; } + (BOOL)isRegularScreen { - return [self isIPad] || (!IS_ZOOMEDMODE && ([self is65InchScreen] || [self is61InchScreen] || [self is55InchScreen])); + if ([@[ + @"iPhone 14 Pro", + @"iPhone 15", + @"iPhone 16", + ] qmui_firstMatchWithBlock:^BOOL(NSString *item) { + return [QMUIHelper.deviceName hasPrefix:item]; + }]) { + return YES; + } + return [self isIPad] || (!IS_ZOOMEDMODE && ([self is67InchScreenAndiPhone14Later] || [self is67InchScreen] || [self is65InchScreen] || [self is61InchScreen] || [self is55InchScreen])); +} + +static NSInteger is69InchScreen = -1; ++ (BOOL)is69InchScreen { + if (is69InchScreen < 0) { + is69InchScreen = CGSizeEqualToSize(CGSizeMake(DEVICE_WIDTH, DEVICE_HEIGHT), self.screenSizeFor69Inch) ? 1 : 0; + } + return is69InchScreen > 0; +} + +static NSInteger is67InchScreenAndiPhone14Later = -1; ++ (BOOL)is67InchScreenAndiPhone14Later { + if (is67InchScreenAndiPhone14Later < 0) { + is67InchScreenAndiPhone14Later = CGSizeEqualToSize(CGSizeMake(DEVICE_WIDTH, DEVICE_HEIGHT), self.screenSizeFor67InchAndiPhone14Later) ? 1 : 0; + } + return is67InchScreenAndiPhone14Later > 0; +} + +static NSInteger is67InchScreen = -1; ++ (BOOL)is67InchScreen { + if (is67InchScreen < 0) { + is67InchScreen = CGSizeEqualToSize(CGSizeMake(DEVICE_WIDTH, DEVICE_HEIGHT), self.screenSizeFor67Inch) ? 1 : 0; + } + return is67InchScreen > 0; } static NSInteger is65InchScreen = -1; + (BOOL)is65InchScreen { if (is65InchScreen < 0) { - // Since iPhone XS Max and iPhone XR share the same resolution, we have to distinguish them using the model identifiers - // 由于 iPhone XS Max 和 iPhone XR 的屏幕宽高是一致的,我们通过机器 Identifier 加以区别 - is65InchScreen = (DEVICE_WIDTH == self.screenSizeFor65Inch.width && DEVICE_HEIGHT == self.screenSizeFor65Inch.height && ([[QMUIHelper deviceModel] isEqualToString:@"iPhone11,4"] || [[QMUIHelper deviceModel] isEqualToString:@"iPhone11,6"])) ? 1 : 0; + // Since iPhone XS Max、iPhone 11 Pro Max and iPhone XR share the same resolution, we have to distinguish them using the model identifiers + // 由于 iPhone XS Max、iPhone 11 Pro Max 这两款机型和 iPhone XR 的屏幕宽高是一致的,我们通过机器 Identifier 加以区别 + is65InchScreen = (DEVICE_WIDTH == self.screenSizeFor65Inch.width && DEVICE_HEIGHT == self.screenSizeFor65Inch.height && !QMUIHelper.is61InchScreen) ? 1 : 0; } return is65InchScreen > 0; } +static NSInteger is63InchScreen = -1; ++ (BOOL)is63InchScreen { + if (is63InchScreen < 0) { + is63InchScreen = CGSizeEqualToSize(CGSizeMake(DEVICE_WIDTH, DEVICE_HEIGHT), self.screenSizeFor63Inch) ? 1 : 0; + } + return is63InchScreen > 0; +} + +static NSInteger is61InchScreenAndiPhone14ProLater = -1; ++ (BOOL)is61InchScreenAndiPhone14ProLater { + if (is61InchScreenAndiPhone14ProLater < 0) { + is61InchScreenAndiPhone14ProLater = (DEVICE_WIDTH == self.screenSizeFor61InchAndiPhone14ProLater.width && DEVICE_HEIGHT == self.screenSizeFor61InchAndiPhone14ProLater.height) ? 1 : 0; + } + return is61InchScreenAndiPhone14ProLater > 0; +} + +static NSInteger is61InchScreenAndiPhone12Later = -1; ++ (BOOL)is61InchScreenAndiPhone12Later { + if (is61InchScreenAndiPhone12Later < 0) { + is61InchScreenAndiPhone12Later = (DEVICE_WIDTH == self.screenSizeFor61InchAndiPhone12Later.width && DEVICE_HEIGHT == self.screenSizeFor61InchAndiPhone12Later.height) ? 1 : 0; + } + return is61InchScreenAndiPhone12Later > 0; +} + static NSInteger is61InchScreen = -1; + (BOOL)is61InchScreen { if (is61InchScreen < 0) { - is61InchScreen = (DEVICE_WIDTH == self.screenSizeFor61Inch.width && DEVICE_HEIGHT == self.screenSizeFor61Inch.height && [[QMUIHelper deviceModel] isEqualToString:@"iPhone11,8"]) ? 1 : 0; + is61InchScreen = (DEVICE_WIDTH == self.screenSizeFor61Inch.width && DEVICE_HEIGHT == self.screenSizeFor61Inch.height && ([[QMUIHelper deviceModel] isEqualToString:@"iPhone11,8"] || [[QMUIHelper deviceModel] isEqualToString:@"iPhone12,1"])) ? 1 : 0; } return is61InchScreen > 0; } @@ -404,6 +660,14 @@ + (BOOL)is55InchScreen { return is55InchScreen > 0; } +static NSInteger is54InchScreen = -1; ++ (BOOL)is54InchScreen { + if (is54InchScreen < 0) { + is54InchScreen = (DEVICE_WIDTH == self.screenSizeFor54Inch.width && DEVICE_HEIGHT == self.screenSizeFor54Inch.height) ? 1 : 0; + } + return is54InchScreen > 0; +} + static NSInteger is47InchScreen = -1; + (BOOL)is47InchScreen { if (is47InchScreen < 0) { @@ -428,10 +692,34 @@ + (BOOL)is35InchScreen { return is35InchScreen > 0; } ++ (CGSize)screenSizeFor69Inch { + return CGSizeMake(440, 956); +} + ++ (CGSize)screenSizeFor67InchAndiPhone14Later { + return CGSizeMake(430, 932);// iPhone 14 Pro Max +} + ++ (CGSize)screenSizeFor67Inch { + return CGSizeMake(428, 926);// iPhone 14 Plus、13 Pro Max、12 Pro Max +} + + (CGSize)screenSizeFor65Inch { return CGSizeMake(414, 896); } ++ (CGSize)screenSizeFor61InchAndiPhone14ProLater { + return CGSizeMake(393, 852); +} + ++ (CGSize)screenSizeFor61InchAndiPhone12Later { + return CGSizeMake(390, 844); +} + ++ (CGSize)screenSizeFor63Inch { + return CGSizeMake(402, 874); +} + + (CGSize)screenSizeFor61Inch { return CGSizeMake(414, 896); } @@ -444,6 +732,10 @@ + (CGSize)screenSizeFor55Inch { return CGSizeMake(414, 736); } ++ (CGSize)screenSizeFor54Inch { + return CGSizeMake(375, 812); +} + + (CGSize)screenSizeFor47Inch { return CGSizeMake(375, 667); } @@ -463,7 +755,7 @@ + (CGFloat)preferredLayoutAsSimilarScreenWidthForIPad { @([self screenSizeFor58Inch].width), @([self screenSizeFor40Inch].width)]; preferredLayoutWidth = SCREEN_WIDTH; - UIWindow *window = [UIApplication sharedApplication].delegate.window ?: [[UIWindow alloc] init];// iOS 9 及以上的系统,新 init 出来的 window 自动被设置为当前 App 的宽度 + UIWindow *window = UIApplication.sharedApplication.delegate.window ?: [[UIWindow alloc] init];// iOS 9 及以上的系统,新 init 出来的 window 自动被设置为当前 App 的宽度 CGFloat windowWidth = CGRectGetWidth(window.bounds); for (NSInteger i = 0; i < widths.count; i++) { if (windowWidth <= widths[i].qmui_CGFloatValue) { @@ -481,26 +773,231 @@ + (UIEdgeInsets)safeAreaInsetsForDeviceWithNotch { } if ([self isIPad]) { - return UIEdgeInsetsMake(0, 0, 20, 0); + return UIEdgeInsetsMake(24, 0, 20, 0); } - UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; - - switch (orientation) { - case UIInterfaceOrientationPortrait: - return UIEdgeInsetsMake(44, 0, 34, 0); + static NSDictionary *> *dict; + if (!dict) { + dict = @{ + // iPhone 16 Pro + @"iPhone17,1": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(62, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 62, 21, 62)], + }, + // iPhone 16 Pro Max + @"iPhone17,2": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(62, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 62, 21, 62)], + }, + // iPhone 16 + @"iPhone17,3": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(59, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 59, 21, 59)], + }, + // iPhone 16 Plus + @"iPhone17,4": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(59, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 59, 21, 59)], + }, + // iPhone 15 + @"iPhone15,4": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(47, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 47, 21, 47)], + }, + @"iPhone15,4-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(48, 0, 28, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 48, 21, 48)], + }, + // iPhone 15 Plus + @"iPhone15,5": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(47, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 47, 21, 47)], + }, + @"iPhone15,5-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(41, 0, 30, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 41, 21, 41)], + }, + // iPhone 15 Pro + @"iPhone16,1": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(59, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 59, 21, 59)], + }, + @"iPhone16,1-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(48, 0, 28, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 48, 21, 48)], + }, + // iPhone 15 Pro Max + @"iPhone16,2": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(59, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 59, 21, 59)], + }, + @"iPhone16,2-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(51, 0, 31, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 51, 21, 51)], + }, + + // iPhone 14 + @"iPhone14,7": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(47, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 47, 21, 47)], + }, + @"iPhone14,7-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(48, 0, 28, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 48, 21, 48)], + }, + // iPhone 14 Plus + @"iPhone14,8": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(47, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 47, 21, 47)], + }, + // iPhone 14 Plus + @"iPhone14,8-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(41, 0, 30, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 41, 21, 41)], + }, + // iPhone 14 Pro + @"iPhone15,2": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(59, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 59, 21, 59)], + }, + @"iPhone15,2-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(48, 0, 28, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 48, 21, 48)], + }, + // iPhone 14 Pro Max + @"iPhone15,3": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(59, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 59, 21, 59)], + }, + @"iPhone15,3-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(51, 0, 31, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 51, 21, 51)], + }, + + // iPhone 13 mini + @"iPhone14,4": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(50, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 50, 21, 50)], + }, + @"iPhone14,4-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(43, 0, 29, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 43, 21, 43)], + }, + // iPhone 13 + @"iPhone14,5": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(47, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 47, 21, 47)], + }, + @"iPhone14,5-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(39, 0, 28, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 39, 21, 39)], + }, + // iPhone 13 Pro + @"iPhone14,2": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(47, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 47, 21, 47)], + }, + @"iPhone14,2-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(39, 0, 28, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 39, 21, 39)], + }, + // iPhone 13 Pro Max + @"iPhone14,3": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(47, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 47, 21, 47)], + }, + @"iPhone14,3-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(41, 0, 29 + 2.0 / 3.0, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 41, 21, 41)], + }, + - case UIInterfaceOrientationPortraitUpsideDown: - return UIEdgeInsetsMake(34, 0, 44, 0); + // iPhone 12 mini + @"iPhone13,1": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(50, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 50, 21, 50)], + }, + @"iPhone13,1-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(43, 0, 29, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 43, 21, 43)], + }, + // iPhone 12 + @"iPhone13,2": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(47, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 47, 21, 47)], + }, + @"iPhone13,2-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(39, 0, 28, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 39, 21, 39)], + }, + // iPhone 12 Pro + @"iPhone13,3": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(47, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 47, 21, 47)], + }, + @"iPhone13,3-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(39, 0, 28, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 39, 21, 39)], + }, + // iPhone 12 Pro Max + @"iPhone13,4": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(47, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 47, 21, 47)], + }, + @"iPhone13,4-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(41, 0, 29 + 2.0 / 3.0, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 41, 21, 41)], + }, + + // iPhone 11 + @"iPhone12,1": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(48, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 48, 21, 48)], + }, + @"iPhone12,1-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(44, 0, 31, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 44, 21, 44)], + }, + // iPhone 11 Pro Max + @"iPhone12,5": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(44, 0, 34, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 44, 21, 44)], + }, + @"iPhone12,5-Zoom": @{ + @(UIInterfaceOrientationPortrait): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(40, 0, 30 + 2.0 / 3.0, 0)], + @(UIInterfaceOrientationLandscapeLeft): [NSValue valueWithUIEdgeInsets:UIEdgeInsetsMake(0, 40, 21, 40)], + }, + }; + } + + NSString *deviceKey = [QMUIHelper deviceModel]; + if (!dict[deviceKey]) { + deviceKey = @"iPhone16,1";// 默认按最新的机型处理,因为新出的设备肯定更大概率与上一代设备相似 + } + if ([QMUIHelper isZoomedMode]) { + deviceKey = [NSString stringWithFormat:@"%@-Zoom", deviceKey]; + } + + NSNumber *orientationKey = nil; + UIInterfaceOrientation orientation = UIApplication.sharedApplication.statusBarOrientation; + switch (orientation) { case UIInterfaceOrientationLandscapeLeft: case UIInterfaceOrientationLandscapeRight: - return UIEdgeInsetsMake(0, 44, 21, 44); - - case UIInterfaceOrientationUnknown: + orientationKey = @(UIInterfaceOrientationLandscapeLeft); + break; default: - return UIEdgeInsetsMake(44, 0, 34, 0); + orientationKey = @(UIInterfaceOrientationPortrait); + break; } + + UIEdgeInsets insets = dict[deviceKey][orientationKey].UIEdgeInsetsValue; + if (orientation == UIInterfaceOrientationPortraitUpsideDown) { + insets = UIEdgeInsetsMake(insets.bottom, insets.left, insets.top, insets.right); + } else if (orientation == UIInterfaceOrientationLandscapeRight) { + insets = UIEdgeInsetsMake(insets.top, insets.right, insets.bottom, insets.left); + } + return insets; } static NSInteger isHighPerformanceDevice = -1; @@ -536,34 +1033,137 @@ + (BOOL)isZoomedMode { return nativeScale > scale; } ++ (BOOL)isDynamicIslandDevice { + if (!IS_IPHONE) return NO; + if ([@[ + @"iPhone 14 Pro", + @"iPhone 15", + @"iPhone 16", + ] qmui_firstMatchWithBlock:^BOOL(NSString *item) { + return [QMUIHelper.deviceName hasPrefix:item]; + }]) { + return YES; + } + return NO; +} + - (void)handleAppSizeWillChange:(NSNotification *)notification { preferredLayoutWidth = -1; } -@end - -@implementation QMUIHelper (UIApplication) ++ (CGSize)applicationSize { + /// applicationFrame 在 iPad 下返回的 size 要比 window 实际的 size 小,这个差值体现在 origin 上,所以用 origin + size 修正得到正确的大小。 + BeginIgnoreDeprecatedWarning + CGRect applicationFrame = [UIScreen mainScreen].applicationFrame; + EndIgnoreDeprecatedWarning + CGSize applicationSize = CGSizeMake(applicationFrame.size.width + applicationFrame.origin.x, applicationFrame.size.height + applicationFrame.origin.y); + if (CGSizeEqualToSize(applicationSize, CGSizeZero)) { + // 实测 MacCatalystApp 通过 [UIScreen mainScreen].applicationFrame 拿不到大小,这里做一下保护 + UIWindow *window = UIApplication.sharedApplication.delegate.window; + if (window) { + applicationSize = window.bounds.size; + } else { + applicationSize = UIWindow.new.bounds.size; + } + } + return applicationSize; +} -+ (void)renderStatusBarStyleDark { - [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault]; ++ (CGFloat)statusBarHeightConstant { + NSString *deviceModel = [QMUIHelper deviceModel]; + + if (!UIApplication.sharedApplication.statusBarHidden) { + return UIApplication.sharedApplication.statusBarFrame.size.height; + } + + if (IS_IPAD) { + return IS_NOTCHED_SCREEN ? 24 : 20; + } + if (!IS_NOTCHED_SCREEN) { + return 20; + } + if (IS_LANDSCAPE) { + return 0; + } + if ([deviceModel isEqualToString:@"iPhone12,1"]) { + // iPhone 13 Mini + return 48; + } + if ([@[ + @"iPhone 14 Pro", + @"iPhone 15", + @"iPhone 16", + ] qmui_firstMatchWithBlock:^BOOL(NSString *item) { + return [QMUIHelper.deviceName hasPrefix:item]; + }]) { + return 54; + } + if (IS_61INCH_SCREEN_AND_IPHONE12 || IS_67INCH_SCREEN) { + return 47; + } + return (IS_54INCH_SCREEN && IOS_VERSION >= 15.0) ? 50 : 44; } -+ (void)renderStatusBarStyleLight { - [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent]; ++ (CGFloat)navigationBarMaxYConstant { + CGFloat result = QMUIHelper.statusBarHeightConstant; + if (IS_IPAD) { + result += 50; + } else if (IS_LANDSCAPE) { + result += PreferredValueForVisualDevice(44, 32); + } else { + result += 44; + if ([@[ + @"iPhone 16 Pro", + ] qmui_firstMatchWithBlock:^BOOL(NSString *item) { + return [QMUIHelper.deviceName hasPrefix:item]; + }]) { + result += 2 + PixelOne;// 56.333 + } else if ([@[ + @"iPhone 14 Pro", + @"iPhone 15", + @"iPhone 16", + ] qmui_firstMatchWithBlock:^BOOL(NSString *item) { + return [QMUIHelper.deviceName hasPrefix:item]; + }]) { + result -= PixelOne;// 53.667 + } + } + return result; } +@end + +@implementation QMUIHelper (UIApplication) + + (void)dimmedApplicationWindow { - UIWindow *window = [[[UIApplication sharedApplication] delegate] window]; + UIWindow *window = UIApplication.sharedApplication.delegate.window; window.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed; [window tintColorDidChange]; } + (void)resetDimmedApplicationWindow { - UIWindow *window = [[[UIApplication sharedApplication] delegate] window]; - window.tintAdjustmentMode = UIViewTintAdjustmentModeNormal; + UIWindow *window = UIApplication.sharedApplication.delegate.window; + window.tintAdjustmentMode = UIViewTintAdjustmentModeAutomatic; [window tintColorDidChange]; } +- (void)handleAppWillEnterForeground:(NSNotification *)notification { + QMUIHelper.sharedInstance.shouldPreventAppearanceUpdating = NO; +} + +- (void)handleAppEnterBackground:(NSNotification *)notification { + QMUIHelper.sharedInstance.shouldPreventAppearanceUpdating = YES; +} + ++ (BOOL)canUpdateAppearance { + // 当配置表被触发时,尚未走到 handleAppDidFinishLaunching,而由于 Objective-C 的 BOOL 类型默认是 NO,所以这里刚好会返回 YES。至于 App 完全启动完成后,就由 notification 的回调来管理 shouldPreventAppearanceUpdating 的值。 + BOOL shouldPrevent = QMUIHelper.sharedInstance.shouldPreventAppearanceUpdating; + if (shouldPrevent) { + return NO; + } + return YES; +} + @end @implementation QMUIHelper (Animation) @@ -596,24 +1196,7 @@ + (NSInteger)numbericOSVersion { } + (NSComparisonResult)compareSystemVersion:(NSString *)currentVersion toVersion:(NSString *)targetVersion { - NSArray *currentVersionArr = [currentVersion componentsSeparatedByString:@"."]; - NSArray *targetVersionArr = [targetVersion componentsSeparatedByString:@"."]; - - NSInteger pos = 0; - - while ([currentVersionArr count] > pos || [targetVersionArr count] > pos) { - NSInteger v1 = [currentVersionArr count] > pos ? [[currentVersionArr objectAtIndex:pos] integerValue] : 0; - NSInteger v2 = [targetVersionArr count] > pos ? [[targetVersionArr objectAtIndex:pos] integerValue] : 0; - if (v1 < v2) { - return NSOrderedAscending; - } - else if (v1 > v2) { - return NSOrderedDescending; - } - pos++; - } - - return NSOrderedSame; + return [currentVersion compare:targetVersion options:NSNumericSearch]; } + (BOOL)isCurrentSystemAtLeastVersion:(NSString *)targetVersion { @@ -626,8 +1209,31 @@ + (BOOL)isCurrentSystemLowerThanVersion:(NSString *)targetVersion { @end +@implementation QMUIHelper (Text) + ++ (CGFloat)baselineOffsetWhenVerticalAlignCenterInHeight:(CGFloat)height withFont:(UIFont *)font { + CGFloat capHeightCenter = height + font.descender - font.capHeight / 2; + CGFloat verticalCenter = height / 2;// 以这一点为中心点 + CGFloat baselineOffset = capHeightCenter - verticalCenter; + // ≤ iOS 16.3.1 的设备上,1pt baseline 会让文本向上移动 2pt,≥ iOS 16.4 均为 1:1 移动。 + if (@available(iOS 16.4, *)) { + } else { + baselineOffset = baselineOffset / 2; + } + return baselineOffset; +} + +@end + @implementation QMUIHelper ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [QMUIHelper sharedInstance];// 确保内部的变量、notification 都正确配置 + }); +} + + (instancetype)sharedInstance { static dispatch_once_t onceToken; static QMUIHelper *instance = nil; @@ -636,12 +1242,15 @@ + (instancetype)sharedInstance { // 先设置默认值,不然可能变量的指针地址错误 instance.keyboardVisible = NO; instance.lastKeyboardHeight = 0; - instance.orientationBeforeChangingByHelper = UIDeviceOrientationUnknown; + instance.lastOrientationChangedByHelper = UIDeviceOrientationUnknown; [[NSNotificationCenter defaultCenter] addObserver:instance selector:@selector(handleKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:instance selector:@selector(handleKeyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:instance selector:@selector(handleAppSizeWillChange:) name:QMUIAppSizeWillChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:instance selector:@selector(handleDeviceOrientationNotification:) name:UIDeviceOrientationDidChangeNotification object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:instance selector:@selector(handleAppWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:instance selector:@selector(handleAppEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; }); return instance; } @@ -655,4 +1264,38 @@ - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } +static NSMutableSet *executedIdentifiers; ++ (BOOL)executeBlock:(void (NS_NOESCAPE ^)(void))block oncePerIdentifier:(NSString *)identifier { + if (!block || identifier.length <= 0) return NO; + @synchronized (self) { + if (!executedIdentifiers) { + executedIdentifiers = NSMutableSet.new; + } + if (![executedIdentifiers containsObject:identifier]) { + [executedIdentifiers addObject:identifier]; + block(); + return YES; + } + return NO; + } +} + ++ (CALayerContentsGravity)layerContentsGravityWithContentMode:(UIViewContentMode)contentMode { + NSDictionary *relationship = @{ + @(UIViewContentModeScaleToFill): kCAGravityResize, + @(UIViewContentModeScaleAspectFit): kCAGravityResizeAspect, + @(UIViewContentModeScaleAspectFill): kCAGravityResizeAspectFill, + @(UIViewContentModeCenter): kCAGravityCenter, + @(UIViewContentModeTop): kCAGravityBottom, + @(UIViewContentModeBottom): kCAGravityTop, + @(UIViewContentModeLeft): kCAGravityLeft, + @(UIViewContentModeRight): kCAGravityRight, + @(UIViewContentModeTopLeft): kCAGravityBottomLeft, + @(UIViewContentModeTopRight): kCAGravityBottomRight, + @(UIViewContentModeBottomLeft): kCAGravityTopLeft, + @(UIViewContentModeBottomRight): kCAGravityTopRight + }; + return relationship[@(contentMode)] ?: kCAGravityCenter; +} + @end diff --git a/QMUI/QMUIKit/QMUICore/QMUILab.h b/QMUI/QMUIKit/QMUICore/QMUILab.h index 85b81b62..7318f700 100644 --- a/QMUI/QMUIKit/QMUICore/QMUILab.h +++ b/QMUI/QMUIKit/QMUICore/QMUILab.h @@ -1,17 +1,16 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUILab.h // QMUIKit // // Created by MoLice on 2019/J/8. -// Copyright © 2019 QMUI Team. All rights reserved. // #ifndef QMUILab_h diff --git a/QMUI/QMUIKit/QMUICore/QMUIRuntime.h b/QMUI/QMUIKit/QMUICore/QMUIRuntime.h index f0a6dd48..2711433a 100644 --- a/QMUI/QMUIKit/QMUICore/QMUIRuntime.h +++ b/QMUI/QMUIKit/QMUICore/QMUIRuntime.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIRuntime.h @@ -109,21 +109,24 @@ OverrideImplementation(Class targetClass, SEL targetSelector, id (^implementatio IMP imp = method_getImplementation(originMethod); BOOL hasOverride = HasOverrideSuperclassMethod(targetClass, targetSelector); - // 以 block 的方式达到实时获取初始方法的 IMP 的目的,从而避免先 swizzle 了 subclass 的方法,再 swizzle superclass 的方法,会发现前者的方法调用不会触发后者 swizzle 后的版本的 bug。 + // 以 block 的方式达到实时获取初始方法的 IMP 的目的,从而避免先 swizzle 了 subclass 的方法,再 swizzle superclass 的方法,会发现前者调用时不会触发后者 swizzle 后的版本的 bug。 IMP (^originalIMPProvider)(void) = ^IMP(void) { IMP result = NULL; - // 如果原本 class 就没人实现那个方法,则返回一个空 block,空 block 虽然没有参数列表,但在业务那边被转换成 IMP 后就算传多个参数进来也不会 crash - if (!imp) { + if (hasOverride) { + result = imp; + } else { + // 如果 superclass 里依然没有实现,则会返回一个 objc_msgForward 从而触发消息转发的流程 + // https://github.com/Tencent/QMUI_iOS/issues/776 + Class superclass = class_getSuperclass(targetClass); + result = class_getMethodImplementation(superclass, targetSelector); + } + + // 这只是一个保底,这里要返回一个空 block 保证非 nil,才能避免用小括号语法调用 block 时 crash + // 空 block 虽然没有参数列表,但在业务那边被转换成 IMP 后就算传多个参数进来也不会 crash + if (!result) { result = imp_implementationWithBlock(^(id selfObject){ QMUILogWarn(([NSString stringWithFormat:@"%@", targetClass]), @"%@ 没有初始实现,%@\n%@", NSStringFromSelector(targetSelector), selfObject, [NSThread callStackSymbols]); }); - } else { - if (hasOverride) { - result = imp; - } else { - Class superclass = class_getSuperclass(targetClass); - result = class_getMethodImplementation(superclass, targetSelector); - } } return result; @@ -154,8 +157,6 @@ ExtendImplementationOfVoidMethodWithoutArguments(Class targetClass, SEL targetSe originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); originSelectorIMP(selfObject, originCMD); - if (![selfObject isKindOfClass:originClass]) return; - implementationBlock(selfObject); }; #if __has_feature(objc_arc) @@ -180,11 +181,7 @@ ExtendImplementationOfVoidMethodWithoutArguments(Class targetClass, SEL targetSe originSelectorIMP = (_returnType (*)(id, SEL))originalIMPProvider();\ _returnType result = originSelectorIMP(selfObject, originCMD);\ \ - if ([selfObject isKindOfClass:originClass]) {\ - return _implementationBlock(selfObject, result);\ - }\ - \ - return result;\ + return _implementationBlock(selfObject, result);\ };\ }); @@ -202,8 +199,6 @@ ExtendImplementationOfVoidMethodWithoutArguments(Class targetClass, SEL targetSe originSelectorIMP = (void (*)(id, SEL, _argumentType))originalIMPProvider();\ originSelectorIMP(selfObject, originCMD, firstArgv);\ \ - if (![selfObject isKindOfClass:originClass]) return;\ - \ _implementationBlock(selfObject, firstArgv);\ };\ }); @@ -215,8 +210,6 @@ ExtendImplementationOfVoidMethodWithoutArguments(Class targetClass, SEL targetSe originSelectorIMP = (void (*)(id, SEL, _argumentType1, _argumentType2))originalIMPProvider();\ originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv);\ \ - if (![selfObject isKindOfClass:originClass]) return;\ - \ _implementationBlock(selfObject, firstArgv, secondArgv);\ };\ }); @@ -234,11 +227,7 @@ ExtendImplementationOfVoidMethodWithoutArguments(Class targetClass, SEL targetSe originSelectorIMP = (_returnType (*)(id, SEL, _argumentType))originalIMPProvider();\ _returnType result = originSelectorIMP(selfObject, originCMD, firstArgv);\ \ - if ([selfObject isKindOfClass:originClass]) {\ - return _implementationBlock(selfObject, firstArgv, result);\ - }\ - \ - return result;\ + return _implementationBlock(selfObject, firstArgv, result);\ };\ }); @@ -249,11 +238,7 @@ ExtendImplementationOfVoidMethodWithoutArguments(Class targetClass, SEL targetSe originSelectorIMP = (_returnType (*)(id, SEL, _argumentType1, _argumentType2))originalIMPProvider();\ _returnType result = originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv);\ \ - if ([selfObject isKindOfClass:originClass]) {\ - return _implementationBlock(selfObject, firstArgv, secondArgv, result);\ - }\ - \ - return result;\ + return _implementationBlock(selfObject, firstArgv, secondArgv, result);\ };\ }); @@ -282,13 +267,16 @@ _QMUITypeEncodingDetectorGenerator(Int, int) _QMUITypeEncodingDetectorGenerator(Short, short) _QMUITypeEncodingDetectorGenerator(Long, long) _QMUITypeEncodingDetectorGenerator(LongLong, long long) +_QMUITypeEncodingDetectorGenerator(NSInteger, NSInteger) _QMUITypeEncodingDetectorGenerator(UnsignedChar, unsigned char) _QMUITypeEncodingDetectorGenerator(UnsignedInt, unsigned int) _QMUITypeEncodingDetectorGenerator(UnsignedShort, unsigned short) _QMUITypeEncodingDetectorGenerator(UnsignedLong, unsigned long) _QMUITypeEncodingDetectorGenerator(UnsignedLongLong, unsigned long long) +_QMUITypeEncodingDetectorGenerator(NSUInteger, NSUInteger) _QMUITypeEncodingDetectorGenerator(Float, float) _QMUITypeEncodingDetectorGenerator(Double, double) +_QMUITypeEncodingDetectorGenerator(CGFloat, CGFloat) _QMUITypeEncodingDetectorGenerator(BOOL, BOOL) _QMUITypeEncodingDetectorGenerator(Void, void) _QMUITypeEncodingDetectorGenerator(Character, char *) @@ -330,3 +318,27 @@ _QMUIGetIvarValueGenerator(Selector, SEL) CG_INLINE id getObjectIvarValue(id object, Ivar ivar) { return object_getIvar(object, ivar); } + + +#pragma mark - Mach-O + +typedef struct classref *classref_t; + +/** + 获取业务项目的所有 class + @param classes 传入 classref_t 变量的指针,会填充结果到里面,然后可以用下标访问。如果只是为了得到总数,可传入 NULL。 + @return class 的总数 + + 例如: + + @code + classref_t *classes = nil; + int count = qmui_getProjectClassList(&classes); + Class class = (__bridge Class)classes[0]; + @endcode + */ +FOUNDATION_EXPORT int qmui_getProjectClassList(classref_t **classes); +/** + 检测是否存在某个dyld image + */ +FOUNDATION_EXPORT BOOL qmui_exists_dyld_image(const char *target_image_name); diff --git a/QMUI/QMUIKit/QMUICore/QMUIRuntime.m b/QMUI/QMUIKit/QMUICore/QMUIRuntime.m index 71c81e0e..7cb5780c 100644 --- a/QMUI/QMUIKit/QMUICore/QMUIRuntime.m +++ b/QMUI/QMUIKit/QMUICore/QMUIRuntime.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUIRuntime.m @@ -14,11 +14,15 @@ // #import "QMUIRuntime.h" +#import "QMUICommonDefines.h" +#import "QMUIHelper.h" +#include +#include @implementation QMUIPropertyDescriptor + (instancetype)descriptorWithProperty:(objc_property_t)property { - QMUIPropertyDescriptor *descriptor = [[QMUIPropertyDescriptor alloc] init]; + QMUIPropertyDescriptor *descriptor = [[self alloc] init]; NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)]; descriptor.name = propertyName; @@ -31,7 +35,7 @@ + (instancetype)descriptorWithProperty:(objc_property_t)property { // setter char *setterChar = property_copyAttributeValue(property, "S"); - NSString *setterString = setterChar != NULL ? [NSString stringWithUTF8String:setterChar] : [QMUIPropertyDescriptor defaultSetterStringWithPropertyName:propertyName]; + NSString *setterString = setterChar != NULL ? [NSString stringWithUTF8String:setterChar] : NSStringFromSelector(setterWithGetter(NSSelectorFromString(propertyName))); descriptor.setter = NSSelectorFromString(setterString); if (setterChar != NULL) { free(setterChar); @@ -93,7 +97,7 @@ - (NSString *)description { [result appendString:self.isAssign ? @"assign" : (self.isWeak ? @"weak" : (self.isStrong ? @"strong" : @"copy"))]; if (self.isReadonly) [result appendString:@", readonly"]; if (![NSStringFromSelector(self.getter) isEqualToString:self.name]) [result appendFormat:@", getter=%@", NSStringFromSelector(self.getter)]; - if (self.setter != NSSelectorFromString([QMUIPropertyDescriptor defaultSetterStringWithPropertyName:self.name])) [result appendFormat:@", setter=%@", NSStringFromSelector(self.setter)]; + if (self.setter != setterWithGetter(NSSelectorFromString(self.name))) [result appendFormat:@", setter=%@", NSStringFromSelector(self.setter)]; [result appendString:@") "]; [result appendString:self.type]; [result appendString:@" "]; @@ -102,14 +106,6 @@ - (NSString *)description { return result.copy; } -+ (NSString *)defaultSetterStringWithPropertyName:(NSString *)propertyName { - NSMutableString *string = [[NSMutableString alloc] initWithString:@"set"]; - [string appendString:[propertyName substringToIndex:1].uppercaseString]; - [string appendString:[propertyName substringFromIndex:1]]; - [string appendString:@":"]; - return string.copy; -} - #define _DetectTypeAndReturn(_type) if (strncmp(@encode(_type), typeEncoding, strlen(@encode(_type))) == 0) return @#_type; + (NSString *)typeWithEncodeString:(NSString *)encodeString { @@ -153,3 +149,89 @@ + (NSString *)typeWithEncodeString:(NSString *)encodeString { } @end + +#ifndef __LP64__ +typedef struct mach_header headerType; +#else +typedef struct mach_header_64 headerType; +#endif + +static BOOL strendswith(const char *str, const char *suffix) { + if (!str || !suffix) return NO; + size_t lenstr = strlen(str); + size_t lensuffix = strlen(suffix); + if (lensuffix > lenstr) return NO; + return strncmp(str + lenstr - lensuffix, suffix, lensuffix) == 0; +} + +static const headerType *getProjectImageHeader(void) { + const uint32_t imageCount = _dyld_image_count(); + NSString *executablePath = NSBundle.mainBundle.executablePath; + if (!executablePath) return nil; + const headerType *target_image_header = 0; +#ifdef IOS18_SDK_ALLOWED +#if DEBUG + // Xcode16之后,优先查找debug.dylib + NSString *debugImagePath = [NSString stringWithFormat:@"%@.debug.dylib", executablePath]; + for (uint32_t i = 0; i < imageCount; i++) { + const char *image_name = _dyld_get_image_name(i); + NSString *imagePath = [NSString stringWithUTF8String:image_name]; + if ([imagePath isEqualToString:debugImagePath]) { + target_image_header = (headerType *)_dyld_get_image_header(i); + break; + } + } + + if (target_image_header) { + return target_image_header; + } +#endif +#endif + for (uint32_t i = 0; i < imageCount; i++) { + const char *image_name = _dyld_get_image_name(i);// name 是一串完整的文件路径,以 image 名结尾 + NSString *imagePath = [NSString stringWithUTF8String:image_name]; + if ([imagePath isEqualToString:executablePath]) { + target_image_header = (headerType *)_dyld_get_image_header(i); + break; + } + } + return target_image_header; +} + +// from https://github.com/opensource-apple/objc4/blob/master/runtime/objc-file.mm +static classref_t *getDataSection(const headerType *machHeader, const char *sectname, size_t *outCount) { + if (!machHeader) return nil; + + unsigned long byteCount = 0; + classref_t *data = (classref_t *)getsectiondata(machHeader, "__DATA", sectname, &byteCount); + if (!data) { + data = (classref_t *)getsectiondata(machHeader, "__DATA_CONST", sectname, &byteCount); + } + if (!data) { + data = (classref_t *)getsectiondata(machHeader, "__DATA_DIRTY", sectname, &byteCount); + } + if (outCount) *outCount = byteCount / sizeof(classref_t); + return data; +} + +int qmui_getProjectClassList(classref_t **classes) { + size_t count = 0; + if (!!classes) { + *classes = getDataSection(getProjectImageHeader(), "__objc_classlist", &count); + } else { + getDataSection(getProjectImageHeader(), "__objc_classlist", &count); + } + return (int)count; +} + + +BOOL qmui_exists_dyld_image(const char *target_image_name) { + const uint32_t imageCount = _dyld_image_count(); + for (uint32_t i = 0; i < imageCount; i++) { + const char *image_name = _dyld_get_image_name(i); + if (strendswith(image_name, target_image_name)) { + return true; + } + } + return false; +} diff --git a/QMUI/QMUIKit/QMUIKit.h b/QMUI/QMUIKit/QMUIKit.h index 5e1e1425..40a5be59 100644 --- a/QMUI/QMUIKit/QMUIKit.h +++ b/QMUI/QMUIKit/QMUIKit.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ /// Automatically created by script in Build Phases @@ -13,7 +13,7 @@ #ifndef QMUIKit_h #define QMUIKit_h -static NSString * const QMUI_VERSION = @"3.2.0"; +static NSString * const QMUI_VERSION = @"4.8.0"; #if __has_include("CAAnimation+QMUI.h") #import "CAAnimation+QMUI.h" @@ -23,6 +23,10 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "CALayer+QMUI.h" #endif +#if __has_include("CALayer+QMUIViewAnimation.h") +#import "CALayer+QMUIViewAnimation.h" +#endif + #if __has_include("NSArray+QMUI.h") #import "NSArray+QMUI.h" #endif @@ -35,6 +39,10 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #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 @@ -59,6 +67,14 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "NSPointerArray+QMUI.h" #endif +#if __has_include("NSRegularExpression+QMUI.h") +#import "NSRegularExpression+QMUI.h" +#endif + +#if __has_include("NSShadow+QMUI.h") +#import "NSShadow+QMUI.h" +#endif + #if __has_include("NSString+QMUI.h") #import "NSString+QMUI.h" #endif @@ -75,6 +91,14 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "QMUIAlertController.h" #endif +#if __has_include("QMUIAnimationHelper.h") +#import "QMUIAnimationHelper.h" +#endif + +#if __has_include("QMUIAppearance.h") +#import "QMUIAppearance.h" +#endif + #if __has_include("QMUIAsset.h") #import "QMUIAsset.h" #endif @@ -87,6 +111,18 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "QMUIAssetsManager.h" #endif +#if __has_include("QMUIBadgeLabel.h") +#import "QMUIBadgeLabel.h" +#endif + +#if __has_include("QMUIBadgeProtocol.h") +#import "QMUIBadgeProtocol.h" +#endif + +#if __has_include("QMUIBarProtocol.h") +#import "QMUIBarProtocol.h" +#endif + #if __has_include("QMUIButton.h") #import "QMUIButton.h" #endif @@ -103,6 +139,10 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "QMUICellSizeKeyCache.h" #endif +#if __has_include("QMUICheckbox.h") +#import "QMUICheckbox.h" +#endif + #if __has_include("QMUICollectionViewPagingLayout.h") #import "QMUICollectionViewPagingLayout.h" #endif @@ -147,6 +187,14 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "QMUIDialogViewController.h" #endif +#if __has_include("QMUIDisplayLinkAnimation.h") +#import "QMUIDisplayLinkAnimation.h" +#endif + +#if __has_include("QMUIEasings.h") +#import "QMUIEasings.h" +#endif + #if __has_include("QMUIEmotionInputManager.h") #import "QMUIEmotionInputManager.h" #endif @@ -159,18 +207,10 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "QMUIEmptyView.h" #endif -#if __has_include("QMUIFillButton.h") -#import "QMUIFillButton.h" -#endif - #if __has_include("QMUIFloatLayoutView.h") #import "QMUIFloatLayoutView.h" #endif -#if __has_include("QMUIGhostButton.h") -#import "QMUIGhostButton.h" -#endif - #if __has_include("QMUIGridView.h") #import "QMUIGridView.h" #endif @@ -219,8 +259,20 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "QMUILabel.h" #endif -#if __has_include("QMUILinkButton.h") -#import "QMUILinkButton.h" +#if __has_include("QMUILayouter.h") +#import "QMUILayouter.h" +#endif + +#if __has_include("QMUILayouterItem.h") +#import "QMUILayouterItem.h" +#endif + +#if __has_include("QMUILayouterLinearHorizontal.h") +#import "QMUILayouterLinearHorizontal.h" +#endif + +#if __has_include("QMUILayouterLinearVertical.h") +#import "QMUILayouterLinearVertical.h" #endif #if __has_include("QMUILog+QMUIConsole.h") @@ -299,16 +351,16 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "QMUIPopupContainerView.h" #endif -#if __has_include("QMUIPopupMenuBaseItem.h") -#import "QMUIPopupMenuBaseItem.h" +#if __has_include("QMUIPopupMenuItem.h") +#import "QMUIPopupMenuItem.h" #endif -#if __has_include("QMUIPopupMenuButtonItem.h") -#import "QMUIPopupMenuButtonItem.h" +#if __has_include("QMUIPopupMenuItemView.h") +#import "QMUIPopupMenuItemView.h" #endif -#if __has_include("QMUIPopupMenuItemProtocol.h") -#import "QMUIPopupMenuItemProtocol.h" +#if __has_include("QMUIPopupMenuItemViewProtocol.h") +#import "QMUIPopupMenuItemViewProtocol.h" #endif #if __has_include("QMUIPopupMenuView.h") @@ -335,8 +387,12 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "QMUISegmentedControl.h" #endif -#if __has_include("QMUISlider.h") -#import "QMUISlider.h" +#if __has_include("QMUISheetPresentationNavigationBar.h") +#import "QMUISheetPresentationNavigationBar.h" +#endif + +#if __has_include("QMUISheetPresentationSupports.h") +#import "QMUISheetPresentationSupports.h" #endif #if __has_include("QMUIStaticTableViewCellData.h") @@ -379,6 +435,18 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "QMUITextView.h" #endif +#if __has_include("QMUITheme.h") +#import "QMUITheme.h" +#endif + +#if __has_include("QMUIThemeManager.h") +#import "QMUIThemeManager.h" +#endif + +#if __has_include("QMUIThemeManagerCenter.h") +#import "QMUIThemeManagerCenter.h" +#endif + #if __has_include("QMUITips.h") #import "QMUITips.h" #endif @@ -403,14 +471,14 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "QMUIToolbarButton.h" #endif -#if __has_include("QMUIVisualEffectView.h") -#import "QMUIVisualEffectView.h" -#endif - #if __has_include("QMUIWeakObjectContainer.h") #import "QMUIWeakObjectContainer.h" #endif +#if __has_include("QMUIWindowSizeMonitor.h") +#import "QMUIWindowSizeMonitor.h" +#endif + #if __has_include("QMUIZoomImageView.h") #import "QMUIZoomImageView.h" #endif @@ -419,6 +487,10 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "UIActivityIndicatorView+QMUI.h" #endif +#if __has_include("UIApplication+QMUI.h") +#import "UIApplication+QMUI.h" +#endif + #if __has_include("UIBarItem+QMUI.h") #import "UIBarItem+QMUI.h" #endif @@ -431,6 +503,10 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "UIBezierPath+QMUI.h" #endif +#if __has_include("UIBlurEffect+QMUI.h") +#import "UIBlurEffect+QMUI.h" +#endif + #if __has_include("UIButton+QMUI.h") #import "UIButton+QMUI.h" #endif @@ -443,10 +519,18 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "UICollectionView+QMUICellSizeKeyCache.h" #endif +#if __has_include("UICollectionViewCell+QMUI.h") +#import "UICollectionViewCell+QMUI.h" +#endif + #if __has_include("UIColor+QMUI.h") #import "UIColor+QMUI.h" #endif +#if __has_include("UIColor+QMUITheme.h") +#import "UIColor+QMUITheme.h" +#endif + #if __has_include("UIControl+QMUI.h") #import "UIControl+QMUI.h" #endif @@ -463,6 +547,10 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "UIImage+QMUI.h" #endif +#if __has_include("UIImage+QMUITheme.h") +#import "UIImage+QMUITheme.h" +#endif + #if __has_include("UIImageView+QMUI.h") #import "UIImageView+QMUI.h" #endif @@ -475,12 +563,16 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "UILabel+QMUI.h" #endif +#if __has_include("UIMenuController+QMUI.h") +#import "UIMenuController+QMUI.h" +#endif + #if __has_include("UINavigationBar+QMUI.h") #import "UINavigationBar+QMUI.h" #endif -#if __has_include("UINavigationBar+Transition.h") -#import "UINavigationBar+Transition.h" +#if __has_include("UINavigationBar+QMUIBarProtocol.h") +#import "UINavigationBar+QMUIBarProtocol.h" #endif #if __has_include("UINavigationController+NavigationBarTransition.h") @@ -491,6 +583,10 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "UINavigationController+QMUI.h" #endif +#if __has_include("UINavigationItem+QMUI.h") +#import "UINavigationItem+QMUI.h" +#endif + #if __has_include("UIScrollView+QMUI.h") #import "UIScrollView+QMUI.h" #endif @@ -499,10 +595,26 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "UISearchBar+QMUI.h" #endif +#if __has_include("UISearchController+QMUI.h") +#import "UISearchController+QMUI.h" +#endif + +#if __has_include("UISlider+QMUI.h") +#import "UISlider+QMUI.h" +#endif + +#if __has_include("UISwitch+QMUI.h") +#import "UISwitch+QMUI.h" +#endif + #if __has_include("UITabBar+QMUI.h") #import "UITabBar+QMUI.h" #endif +#if __has_include("UITabBar+QMUIBarProtocol.h") +#import "UITabBar+QMUIBarProtocol.h" +#endif + #if __has_include("UITabBarItem+QMUI.h") #import "UITabBarItem+QMUI.h" #endif @@ -523,22 +635,62 @@ static NSString * const QMUI_VERSION = @"3.2.0"; #import "UITableViewCell+QMUI.h" #endif +#if __has_include("UITableViewHeaderFooterView+QMUI.h") +#import "UITableViewHeaderFooterView+QMUI.h" +#endif + #if __has_include("UITextField+QMUI.h") #import "UITextField+QMUI.h" #endif +#if __has_include("UITextInputTraits+QMUI.h") +#import "UITextInputTraits+QMUI.h" +#endif + #if __has_include("UITextView+QMUI.h") #import "UITextView+QMUI.h" #endif +#if __has_include("UIToolbar+QMUI.h") +#import "UIToolbar+QMUI.h" +#endif + +#if __has_include("UITraitCollection+QMUI.h") +#import "UITraitCollection+QMUI.h" +#endif + #if __has_include("UIView+QMUI.h") #import "UIView+QMUI.h" #endif +#if __has_include("UIView+QMUIBadge.h") +#import "UIView+QMUIBadge.h" +#endif + +#if __has_include("UIView+QMUIBorder.h") +#import "UIView+QMUIBorder.h" +#endif + +#if __has_include("UIView+QMUITheme.h") +#import "UIView+QMUITheme.h" +#endif + #if __has_include("UIViewController+QMUI.h") #import "UIViewController+QMUI.h" #endif +#if __has_include("UIViewController+QMUITheme.h") +#import "UIViewController+QMUITheme.h" +#endif + +#if __has_include("UIVisualEffect+QMUITheme.h") +#import "UIVisualEffect+QMUITheme.h" +#endif + +#if __has_include("UIVisualEffectView+QMUI.h") +#import "UIVisualEffectView+QMUI.h" +#endif + #if __has_include("UIWindow+QMUI.h") #import "UIWindow+QMUI.h" #endif diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.h b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.h index 48c90b32..e3c6a6f8 100644 --- a/QMUI/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.h +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUICommonTableViewController.h @@ -24,12 +24,12 @@ extern NSString *const QMUICommonTableViewControllerSectionFooterIdentifier; /** * 可作为项目内所有 `UITableViewController` 的基类,注意是继承自 `QMUICommonViewController` 而不是 `UITableViewController`。 * - * 一般通过 `initWithStyle:` 方法初始化,对于要生成 `UITableViewStylePlain` 类型的列表,推荐使用 `init:` 方法。 + * 一般通过 `initWithStyle:` 方法初始化,对于要生成 `UITableViewStylePlain` 类型的列表,推荐使用 `init` 方法。 * * 提供的功能包括: * * 1. 集成 `QMUISearchController`,可通过属性 `shouldShowSearchBar` 来快速为列表生成一个 searchBar 及 searchController,具体请查看 QMUICommonTableViewController (Search)。 - * 2. 支持仅设置 tableView:titleForHeaderInSection: 就能自动生成 sectionHeader,无需编写 viewForHeaderInSection:、heightForHeaderInSection: 等方法。 + * 2. 支持仅设置 tableView:titleForHeaderInSection: 就能自动生成 sectionHeader 并且样式统一由配置表设置,无需编写 viewForHeaderInSection:、heightForHeaderInSection: 等方法。 * 3. 自带一个 QMUIEmptyView,作为 tableView 的 subview,可用于显示 loading、空或错误提示语等。 * * @note emptyView 会从 tableHeaderView 的下方开始布局到 tableView 最底部,因此它会遮挡 tableHeaderView 之外的部分(比如 tableFooterView 和 cells ),你可以重写 layoutEmptyView 来改变这个布局方式 @@ -49,8 +49,13 @@ extern NSString *const QMUICommonTableViewControllerSectionFooterIdentifier; /// 获取当前的 `UITableViewStyle` @property(nonatomic, assign, readonly) UITableViewStyle style; -/// 获取当前的 tableView -@property(nonatomic, strong, readonly, null_resettable) IBOutlet QMUITableView *tableView; +/// 当前的 tableView,如果需要使用自定义的 tableView class,可重写 initTableView 并在里面通过 self.tableView = xxx 为 tableView 赋值,注意需要自行指定 dataSource 和 delegate 但不需要 add 到 self.view 上。 +/// @note 直接把自定义 tableView 赋值给 self.tableView 也可以,但 QMUI 将会多余地创建一次 QMUITableView,会造成浪费。 +#if !TARGET_INTERFACE_BUILDER +@property(nonatomic, strong, null_resettable) IBOutlet __kindof QMUITableView *tableView; +#else +@property(nonatomic, strong, null_resettable) IBOutlet QMUITableView *tableView; +#endif - (void)hideTableHeaderViewInitialIfCanWithAnimated:(BOOL)animated force:(BOOL)force; @@ -60,11 +65,17 @@ extern NSString *const QMUICommonTableViewControllerSectionFooterIdentifier; @interface QMUICommonTableViewController (QMUISubclassingHooks) /** - * 初始化tableView,在initSubViews的时候被自动调用。 + * 初始化 tableView,在 tableView getter 被调用时会触发,可重写这个方法并通过 self.tableView = xxx 来指定自定义的 tableView class,注意需要自行指定 dataSource 和 delegate,但不需要手动 add 到 self.view 上。一般情况下,有关tableView的设置属性的代码都应该写在这里。 * - * 一般情况下,有关tableView的设置属性的代码都应该写在这里。 + * @note 如果要为 self.tableView = xxx 赋值则不需要调用 super。 + * @example + * - (void)initTableView { + * self.tableView = [MyTableView alloc] initWithFrame:self.view.bounds style:self.style]; + * self.tableView.dataSource = self; + * self.tableView.delegate = self; + * } */ -- (void)initTableView NS_REQUIRES_SUPER; +- (void)initTableView; /** * 布局 tableView 的方法独立抽取出来,方便子类在需要自定义 tableView.frame 时能重写并且屏蔽掉 super 的代码。如果不独立一个方法而是放在 viewDidLayoutSubviews 里,子类就很难屏蔽 super 里对 tableView.frame 的修改。 diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m index 9339bf9b..ae17a186 100644 --- a/QMUI/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUICommonTableViewController.m @@ -22,15 +22,14 @@ #import "UITableView+QMUI.h" #import "UICollectionView+QMUI.h" #import "UIView+QMUI.h" +#import "UIViewController+QMUI.h" NSString *const QMUICommonTableViewControllerSectionHeaderIdentifier = @"QMUISectionHeaderView"; NSString *const QMUICommonTableViewControllerSectionFooterIdentifier = @"QMUISectionFooterView"; @interface QMUICommonTableViewController () -@property(nonatomic, strong, readwrite) QMUITableView *tableView; @property(nonatomic, assign) BOOL hasHideTableHeaderViewInitial; - @end @@ -65,13 +64,8 @@ - (void)didInitializeWithStyle:(UITableViewStyle)style { - (void)dealloc { // 用下划线而不是self.xxx来访问tableView,避免dealloc时self.view尚未被加载,此时调用self.tableView反而会触发loadView - _tableView.delegate = nil; _tableView.dataSource = nil; - - if (@available(iOS 11.0, *)) { - } else { - [_tableView removeObserver:self forKeyPath:@"contentInset"]; - } + _tableView.delegate = nil; } - (NSString *)description { @@ -80,12 +74,12 @@ - (NSString *)description { return [super description]; } - NSString *result = [NSString stringWithFormat:@"%@\ntableView:\t\t\t\t%@", [super description], self.tableView]; - NSInteger sections = [self.tableView.dataSource numberOfSectionsInTableView:self.tableView]; + NSString *tableView = [NSString stringWithFormat:@"<%@: %p>", NSStringFromClass(self.tableView.class), self.tableView]; + NSString *result = [NSString stringWithFormat:@"%@\ntableView:\t\t\t\t%@", [super description], tableView]; + NSInteger sections = [self.tableView.dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)] ? [self.tableView.dataSource numberOfSectionsInTableView:self.tableView] : 1; if (sections > 0) { NSMutableString *sectionCountString = [[NSMutableString alloc] init]; - [sectionCountString appendFormat:@"\ndataCount(%@):\t\t\t\t(\n", @(sections)]; - NSInteger sections = [self.tableView.dataSource numberOfSectionsInTableView:self.tableView]; + [sectionCountString appendFormat:@"\ndataCount(%@):\t\t\t(\n", @(sections)]; for (NSInteger i = 0; i < sections; i++) { NSInteger rows = [self.tableView.dataSource tableView:self.tableView numberOfRowsInSection:i]; [sectionCountString appendFormat:@"\t\t\t\t\t\t\tsection%@ - rows%@%@\n", @(i), @(rows), i < sections - 1 ? @"," : @""]; @@ -101,14 +95,8 @@ - (NSString *)description { - (void)viewDidLoad { [super viewDidLoad]; - UIColor *backgroundColor = nil; - if (self.style == UITableViewStylePlain) { - backgroundColor = TableViewBackgroundColor; - } else { - backgroundColor = TableViewGroupedBackgroundColor; - } - if (backgroundColor) { - self.view.backgroundColor = backgroundColor; + if (self.tableView.backgroundColor) { + self.view.backgroundColor = self.tableView.backgroundColor;// 让 self.view 背景色跟随不同的 UITableViewStyle 走 } } @@ -120,7 +108,9 @@ - (void)initSubviews { - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if (!self.tableView.allowsMultipleSelection) { - [self.tableView qmui_clearsSelection]; + [self qmui_animateAlongsideTransition:^(id _Nonnull context) { + [self.tableView qmui_clearsSelection]; + } completion:nil]; } } @@ -134,24 +124,42 @@ - (void)viewDidLayoutSubviews { [self layoutEmptyView]; } -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if ([keyPath isEqualToString:@"contentInset"]) { - [self handleTableViewContentInsetChangeEvent]; - } -} - #pragma mark - 工具方法 -- (QMUITableView *)tableView { +@synthesize tableView = _tableView; +- (__kindof QMUITableView *)tableView { if (!_tableView) { [self loadViewIfNeeded]; } return _tableView; } +- (void)setTableView:(__kindof QMUITableView *)tableView { + if (_tableView != tableView) { + if (_tableView) { + // 这里不用移除 delegate、dataSource,因为原本的值也不一定是指向 self,而且可能是个 QMUIMultipleDelegate,反正这两个属性都是 weak 的 + if (self.isViewLoaded && _tableView.superview == self.view) { + [_tableView removeFromSuperview]; + } + } + + _tableView = tableView; + [_tableView registerClass:[QMUITableViewHeaderFooterView class] forHeaderFooterViewReuseIdentifier:QMUICommonTableViewControllerSectionHeaderIdentifier]; + [_tableView registerClass:[QMUITableViewHeaderFooterView class] forHeaderFooterViewReuseIdentifier:QMUICommonTableViewControllerSectionFooterIdentifier]; + + // 从 nib 初始化的界面,loadView 里 tableView 已经被加到 self.view 上了,但此时 loadView 尚未结束,所以 isViewLoaded 为 NO。这种场景不需要自己 addSubview,也不应该去调用 self.view 触发 loadView + // https://github.com/Tencent/QMUI_iOS/issues/1156 + if (tableView.superview && self.nibName && !self.isViewLoaded) { + } else { + // 触发 loadView + [self.view addSubview:_tableView]; + } + } +} + - (void)hideTableHeaderViewInitialIfCanWithAnimated:(BOOL)animated force:(BOOL)force { if (self.tableView.tableHeaderView && [self shouldHideTableHeaderViewInitial] && (force || !self.hasHideTableHeaderViewInitial)) { - CGPoint contentOffset = CGPointMake(self.tableView.contentOffset.x, -self.tableView.qmui_contentInset.top + CGRectGetHeight(self.tableView.tableHeaderView.frame)); + CGPoint contentOffset = CGPointMake(self.tableView.contentOffset.x, -self.tableView.adjustedContentInset.top + CGRectGetHeight(self.tableView.tableHeaderView.frame)); [self.tableView setContentOffset:contentOffset animated:animated]; self.hasHideTableHeaderViewInitial = YES; } @@ -159,7 +167,9 @@ - (void)hideTableHeaderViewInitialIfCanWithAnimated:(BOOL)animated force:(BOOL)f - (void)contentSizeCategoryDidChanged:(NSNotification *)notification { [super contentSizeCategoryDidChanged:notification]; - [self.tableView reloadData]; + if (self.viewLoaded) { + [self.tableView reloadData]; + } } #pragma mark - 空列表视图 QMUIEmptyView @@ -171,29 +181,17 @@ - (void)handleTableViewContentInsetChangeEvent { } - (void)showEmptyView { - if (!self.emptyView) { - self.emptyView = [[QMUIEmptyView alloc] init]; - } [self.tableView addSubview:self.emptyView]; [self layoutEmptyView]; } -- (void)hideEmptyView { - [self.emptyView removeFromSuperview]; -} - // 注意,emptyView 的布局依赖于 tableView.contentInset,因此我们必须监听 tableView.contentInset 的变化以及时更新 emptyView 的布局 - (BOOL)layoutEmptyView { - if (!self.emptyView || !self.emptyView.superview) { + if (!_emptyView || !_emptyView.superview) { return NO; } - UIEdgeInsets insets = self.tableView.contentInset; - if (@available(iOS 11, *)) { - if (self.tableView.contentInsetAdjustmentBehavior != UIScrollViewContentInsetAdjustmentNever) { - insets = self.tableView.adjustedContentInset; - } - } + UIEdgeInsets insets = self.tableView.adjustedContentInset; // 当存在 tableHeaderView 时,emptyView 的高度为 tableView 的高度减去 headerView 的高度 if (self.tableView.tableHeaderView) { @@ -240,26 +238,26 @@ - (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { if ([tableView.delegate respondsToSelector:@selector(tableView:viewForHeaderInSection:)]) { + // 系统的行为是当你实现了 tableView:viewForHeaderInSection: 后,无论你在其中是否 return nil,唯一隐藏 header 的方式就是在 tableView:heightForHeaderInSection: 里返回 0/CGFLOAT_MAX,所以这里需要判断返回值非空就用 self-sizing 自动计算,否则都视为不需要显示 header UIView *view = [tableView.delegate tableView:tableView viewForHeaderInSection:section]; if (view) { - CGFloat height = [view sizeThatFits:CGSizeMake(CGRectGetWidth(tableView.bounds) - UIEdgeInsetsGetHorizontalValue(tableView.qmui_safeAreaInsets), CGFLOAT_MAX)].height; - return height; + return UITableViewAutomaticDimension; } } - // 分别测试过 iOS 11 前后的系统版本,最终总结,对于 Plain 类型的 tableView 而言,要去掉 header / footer 请使用 0,对于 Grouped 类型的 tableView 而言,要去掉 header / footer 请使用 CGFLOAT_MIN - return tableView.style == UITableViewStylePlain ? 0 : TableViewGroupedSectionHeaderDefaultHeight; + // 分别测试过 iOS 13 及以下的所有版本,最终总结,对于 Plain 类型的 tableView 而言,要去掉 header / footer 请使用 0,对于 Grouped 类型的 tableView 而言,要去掉 header / footer 请使用 CGFLOAT_MIN + return PreferredValueForTableViewStyle(tableView.style, 0, TableViewGroupedSectionHeaderDefaultHeight, TableViewInsetGroupedSectionHeaderDefaultHeight); } - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { if ([tableView.delegate respondsToSelector:@selector(tableView:viewForFooterInSection:)]) { + // 系统的行为是当你实现了 tableView:viewForFooterInSection: 后,无论你在其中是否 return nil,唯一隐藏 footer 的方式就是在 tableView:heightForFooterInSection: 里返回 0/CGFLOAT_MAX,所以这里需要判断返回值非空就用 self-sizing 自动计算,否则都视为不需要显示 footer UIView *view = [tableView.delegate tableView:tableView viewForFooterInSection:section]; if (view) { - CGFloat height = [view sizeThatFits:CGSizeMake(CGRectGetWidth(tableView.bounds) - UIEdgeInsetsGetHorizontalValue(tableView.qmui_safeAreaInsets), CGFLOAT_MAX)].height; - return height; + return UITableViewAutomaticDimension; } } - // 分别测试过 iOS 11 前后的系统版本,最终总结,对于 Plain 类型的 tableView 而言,要去掉 header / footer 请使用 0,对于 Grouped 类型的 tableView 而言,要去掉 header / footer 请使用 CGFLOAT_MIN - return tableView.style == UITableViewStylePlain ? 0 : TableViewGroupedSectionFooterDefaultHeight; + // 分别测试过 iOS 13 及以下的所有版本,最终总结,对于 Plain 类型的 tableView 而言,要去掉 header / footer 请使用 0,对于 Grouped 类型的 tableView 而言,要去掉 header / footer 请使用 CGFLOAT_MIN + return PreferredValueForTableViewStyle(tableView.style, 0, TableViewGroupedSectionFooterDefaultHeight, TableViewInsetGroupedSectionFooterDefaultHeight); } // 是否有定义某个section的header title @@ -290,12 +288,9 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N /** * 监听 contentInset 的变化以及时更新 emptyView 的布局,详见 layoutEmptyView 方法的注释 - * 该 delegate 方法仅在 iOS 11 及之后存在,之前的 iOS 版本使用 KVO 的方式实现监听,详见 initTableView 方法里的相关代码 */ - (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView { - if (scrollView != self.tableView) { - return; - } + if (_tableView != scrollView) return; [self handleTableViewContentInsetChangeEvent]; } @@ -306,21 +301,11 @@ @implementation QMUICommonTableViewController (QMUISubclassingHooks) - (void)initTableView { if (!_tableView) { - _tableView = [[QMUITableView alloc] initWithFrame:self.view.bounds style:self.style]; - self.tableView.delegate = self; - self.tableView.dataSource = self; - [self.tableView registerClass:[QMUITableViewHeaderFooterView class] forHeaderFooterViewReuseIdentifier:QMUICommonTableViewControllerSectionHeaderIdentifier]; - [self.tableView registerClass:[QMUITableViewHeaderFooterView class] forHeaderFooterViewReuseIdentifier:QMUICommonTableViewControllerSectionFooterIdentifier]; - [self.view addSubview:self.tableView]; - } - - if (@available(iOS 11, *)) { - } else { - /** - * 监听 contentInset 的变化以及时更新 emptyView 的布局,详见 layoutEmptyView 方法的注释 - * iOS 11 及之后使用 UIScrollViewDelegate 的 scrollViewDidChangeAdjustedContentInset: 来监听 - */ - [self.tableView addObserver:self forKeyPath:@"contentInset" options:NSKeyValueObservingOptionOld context:nil]; + self.tableView = [[QMUITableView alloc] initWithFrame:self.isViewLoaded ? self.view.bounds : CGRectZero style:self.style]; + // setDataSource: 不会触发 tableView reload,而 setDelegate: 可以,所以把 setDelegate: 放在后面,保证 reload 时能访问到 dataSource 里的数据源。 + // 否则如果列表开启了 estimated,然后在 viewDidLoad 里设置 tableHeaderView,则 setTableHeaderView: 时由于 setDataSource: 后 tableView 其实没再刷新过,所以内部依然认为 numberOfSections 是默认的1,于是就会去调用 numberOfRows,如果此时 numberOfRows 里用 indexPath 作为下标去访问数据源就会产生越界(因为此时数据源可能还是空的) + _tableView.dataSource = self; + _tableView.delegate = self; } } diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUICommonViewController.h b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonViewController.h index e18abec7..3275e0d9 100644 --- a/QMUI/QMUIKit/QMUIMainFrame/QMUICommonViewController.h +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonViewController.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUICommonViewController.h @@ -26,20 +26,20 @@ NS_ASSUME_NONNULL_BEGIN /** * 可作为项目内所有 `UIViewController` 的基类,提供的功能包括: * - * 1. 自带顶部标题控件 `QMUINavigationTitleView`,支持loading、副标题、下拉菜单,设置标题依然使用系统的 `setTitle:` 方法 + * 1. 自带顶部标题控件 `QMUINavigationTitleView`,支持loading、副标题、下拉菜单,设置标题依然使用系统的 `-[UIViewController setTitle:]` 或 `-[UINavigationItem setTitle:]` 方法 * * 2. 自带空界面控件 `QMUIEmptyView`,支持显示loading、空文案、操作按钮 * - * 3. 自动在 `dealloc` 时移除所有注册到 `NSNotificationCenter` 里的监听,避免野指针 crash - * - * 4. 统一约定的常用接口,例如初始化 subview、设置顶部 `navigationItem`、底部 `toolbarItem`、响应系统的动态字体大小变化、...,从而保证相同类型的代码集中到同一个方法内,避免多人交叉维护时代码分散难以查找 + * 3. 统一约定的常用接口,例如初始化 subview、设置顶部 `navigationItem`、底部 `toolbarItem`、响应系统的动态字体大小变化、...,从而保证相同类型的代码集中到同一个方法内,避免多人交叉维护时代码分散难以查找 * - * 5. 配合 `QMUINavigationController` 使用时,可以得到 `willPopInNavigationControllerWithAnimated:`、`didPopInNavigationControllerWithAnimated:` 这两个时机 + * 4. 配合 `QMUINavigationController` 使用时,可以得到 `willPopInNavigationControllerWithAnimated:`、`didPopInNavigationControllerWithAnimated:` 这两个时机 * * @see QMUINavigationTitleView * @see QMUIEmptyView */ -@interface QMUICommonViewController : UIViewController +@interface QMUICommonViewController : UIViewController { + QMUIEmptyView *_emptyView; +} - (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER; - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER; @@ -63,7 +63,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, assign) UIInterfaceOrientationMask supportedOrientationMask; /** - * 空列表控件,支持显示提示文字、loading、操作按钮 + * 空列表控件,支持显示提示文字、loading、操作按钮,该属性懒加载 */ @property(nullable, nonatomic, strong) QMUIEmptyView *emptyView; @@ -180,9 +180,9 @@ NS_ASSUME_NONNULL_BEGIN @property(nullable, nonatomic, strong, readonly) QMUIKeyboardManager *hideKeyboardManager; /** - * 当用户点击界面上某个 view 时,如果此时键盘处于升起状态,则可通过重写这个方法并返回一个 YES 来达到“点击空白区域自动降下键盘”的需求。默认返回 NO,也即不处理键盘。 - * @warning 注意如果被点击的 view 本身消耗了事件(iOS 11 下测试得到这种类型的所有系统的 view 仅有 UIButton 和 UISwitch),则这个方法并不会被触发。 - * @warning 有可能参数传进去的 view 是某个 subview 的 subview,所以建议用 isDescendantOfView: 来判断是否点到了某个目标 subview + * 当用户点击界面上某个非 UITextField、UITextView 的 view 时,如果此时键盘处于升起状态,则可通过重写这个方法并返回一个 YES 来达到“点击空白区域自动降下键盘”的需求。默认返回 NO,也即不处理键盘。 + * @note 注意如果被点击的 view 本身消耗了事件(iOS 11 下测试得到这种类型的所有系统的 view 仅有 UIButton 和 UISwitch),则这个方法并不会被触发。 + * @note 有可能参数传进去的 view 是某个 subview 的 subview,所以建议用 isDescendantOfView: 来判断是否点到了某个目标 subview */ - (BOOL)shouldHideKeyboardWhenTouchInView:(nullable UIView *)view; diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUICommonViewController.m b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonViewController.m index 80a770f1..24106ae7 100644 --- a/QMUI/QMUIKit/QMUIMainFrame/QMUICommonViewController.m +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonViewController.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUICommonViewController.m @@ -62,29 +62,31 @@ - (void)didInitialize { self.titleView.title = self.title;// 从 storyboard 初始化的话,可能带有 self.title 的值 self.navigationItem.titleView = self.titleView; - self.hidesBottomBarWhenPushed = HidesBottomBarWhenPushedInitially; - // 不管navigationBar的backgroundImage如何设置,都让布局撑到屏幕顶部,方便布局的统一 self.extendedLayoutIncludesOpaqueBars = YES; self.supportedOrientationMask = SupportedOrientationMask; + if (QMUICMIActivated) { + self.hidesBottomBarWhenPushed = HidesBottomBarWhenPushedInitially; + self.qmui_preferredStatusBarStyleBlock = ^UIStatusBarStyle{ + return DefaultStatusBarStyle; + }; + } + + self.qmui_prefersHomeIndicatorAutoHiddenBlock = ^BOOL{ + return NO; + }; + + // 动态字体notification [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contentSizeCategoryDidChanged:) name:UIContentSizeCategoryDidChangeNotification object:nil]; } -- (void)setTitle:(NSString *)title { - [super setTitle:title]; - self.titleView.title = title; -} - - (void)viewDidLoad { [super viewDidLoad]; - if (!self.view.backgroundColor) { - UIColor *backgroundColor = UIColorForBackground; - if (backgroundColor) { - self.view.backgroundColor = backgroundColor; - } + if (!self.view.backgroundColor && QMUICMIActivated) {// nib 里可能设置了,所以做个 if 的判断 + self.view.backgroundColor = UIColorForBackground; } // 点击空白区域降下键盘 QMUICommonViewController (QMUIKeyboard) @@ -108,10 +110,8 @@ - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; // fix iOS 11 and later, shouldHideKeyboardWhenTouchInView: will not work when calling becomeFirstResponder in UINavigationController.rootViewController.viewDidLoad // https://github.com/Tencent/QMUI_iOS/issues/495 - if (@available(iOS 11.0, *)) { - if (self.hideKeyboardManager && [QMUIKeyboardManager isKeyboardVisible]) { - self.hideKeyboardTapGestureRecognizer.enabled = YES; - } + if (self.hideKeyboardManager && [QMUIKeyboardManager isKeyboardVisible]) { + self.hideKeyboardTapGestureRecognizer.enabled = YES; } } @@ -128,19 +128,25 @@ - (void)viewWillAppear:(BOOL)animated { #pragma mark - 空列表视图 QMUIEmptyView -- (void)showEmptyView { - if (!self.emptyView) { - self.emptyView = [[QMUIEmptyView alloc] initWithFrame:self.view.bounds]; +@synthesize emptyView = _emptyView; + +- (QMUIEmptyView *)emptyView { + if (!_emptyView && self.isViewLoaded) { + _emptyView = [[QMUIEmptyView alloc] initWithFrame:self.view.bounds]; } + return _emptyView; +} + +- (void)showEmptyView { [self.view addSubview:self.emptyView]; } - (void)hideEmptyView { - [self.emptyView removeFromSuperview]; + [_emptyView removeFromSuperview]; } - (BOOL)isEmptyViewShowing { - return self.emptyView && self.emptyView.superview; + return _emptyView && _emptyView.superview; } - (void)showEmptyViewWithLoading { @@ -184,7 +190,7 @@ - (void)showEmptyViewWithLoading:(BOOL)showLoading } - (BOOL)layoutEmptyView { - if (self.emptyView) { + if (_emptyView) { // 由于为self.emptyView设置frame时会调用到self.view,为了避免导致viewDidLoad提前触发,这里需要判断一下self.view是否已经被初始化 BOOL viewDidLoad = self.emptyView.superview && [self isViewLoaded]; if (viewDidLoad) { @@ -210,12 +216,6 @@ - (UIInterfaceOrientationMask)supportedInterfaceOrientations { return self.supportedOrientationMask; } -#pragma mark - HomeIndicator - -- (BOOL)prefersHomeIndicatorAutoHidden { - return NO; -} - @end @implementation QMUICommonViewController (QMUISubclassingHooks) @@ -245,49 +245,28 @@ - (void)updateNavigationBarAppearance { UINavigationBar *navigationBar = self.navigationController.navigationBar; if (!navigationBar) return; - BeginIgnoreDeprecatedWarning - if ([self respondsToSelector:@selector(shouldSetStatusBarStyleLight)]) { - if ([self shouldSetStatusBarStyleLight]) { - if ([[UIApplication sharedApplication] statusBarStyle] < UIStatusBarStyleLightContent) { - [QMUIHelper renderStatusBarStyleLight]; - } - } else { - if ([[UIApplication sharedApplication] statusBarStyle] >= UIStatusBarStyleLightContent) { - [QMUIHelper renderStatusBarStyleDark]; - } - } + if ([self respondsToSelector:@selector(qmui_navigationBarBackgroundImage)]) { + [navigationBar setBackgroundImage:[self qmui_navigationBarBackgroundImage] forBarMetrics:UIBarMetricsDefault]; } - EndIgnoreDeprecatedWarning - - if ([self respondsToSelector:@selector(navigationBarBackgroundImage)]) { - [navigationBar setBackgroundImage:[self navigationBarBackgroundImage] forBarMetrics:UIBarMetricsDefault]; + if ([self respondsToSelector:@selector(qmui_navigationBarBarTintColor)]) { + navigationBar.barTintColor = [self qmui_navigationBarBarTintColor]; } - if ([self respondsToSelector:@selector(navigationBarBarTintColor)]) { - navigationBar.barTintColor = [self navigationBarBarTintColor]; + if ([self respondsToSelector:@selector(qmui_navigationBarStyle)]) { + navigationBar.barStyle = [self qmui_navigationBarStyle]; } - if ([self respondsToSelector:@selector(navigationBarShadowImage)]) { - navigationBar.shadowImage = [self navigationBarShadowImage]; + if ([self respondsToSelector:@selector(qmui_navigationBarShadowImage)]) { + navigationBar.shadowImage = [self qmui_navigationBarShadowImage]; } - if ([self respondsToSelector:@selector(navigationBarTintColor)]) { - navigationBar.tintColor = [self navigationBarTintColor]; + if ([self respondsToSelector:@selector(qmui_navigationBarTintColor)]) { + navigationBar.tintColor = [self qmui_navigationBarTintColor]; } - if ([self respondsToSelector:@selector(titleViewTintColor)]) { - self.titleView.tintColor = [self titleViewTintColor]; + if ([self respondsToSelector:@selector(qmui_titleViewTintColor)]) { + self.titleView.tintColor = [self qmui_titleViewTintColor]; } } #pragma mark - -BeginIgnoreClangWarning(-Wdeprecated-implementations) -- (BOOL)shouldSetStatusBarStyleLight { - return StatusbarStyleLightInitially; -} -EndIgnoreClangWarning - -- (UIStatusBarStyle)preferredStatusBarStyle { - return StatusbarStyleLightInitially ? UIStatusBarStyleLightContent : UIStatusBarStyleDefault; -} - - (BOOL)preferredNavigationBarHidden { return NavigationBarHiddenInitially; } diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUINavigationController.h b/QMUI/QMUIKit/QMUIMainFrame/QMUINavigationController.h index a0d2a7a8..9abca58c 100644 --- a/QMUI/QMUIKit/QMUIMainFrame/QMUINavigationController.h +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUINavigationController.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUINavigationController.h @@ -18,11 +18,6 @@ @interface QMUINavigationController : UINavigationController -/** - * 初始化时调用的方法,会在 initWithNibName:bundle: 和 initWithCoder: 这两个指定的初始化方法中被调用,所以子类如果需要同时支持两个初始化方法,则建议把初始化时要做的事情放到这个方法里。否则仅需重写要支持的那个初始化方法即可。 - */ -- (void)didInitialize NS_REQUIRES_SUPER; - @end @interface QMUINavigationController (UISubclassingHooks) @@ -52,7 +47,24 @@ * @param viewControllerWillDisappear 手势返回中顶部的那个 vc * @param viewControllerWillAppear 手势返回中背后的那个 vc */ -- (void)navigationController:(nonnull QMUINavigationController *)navigationController poppingByInteractiveGestureRecognizer:(nullable UIScreenEdgePanGestureRecognizer *)gestureRecognizer viewControllerWillDisappear:(nullable UIViewController *)viewControllerWillDisappear viewControllerWillAppear:(nullable UIViewController *)viewControllerWillAppear; +- (void)navigationController:(nonnull QMUINavigationController *)navigationController +poppingByInteractiveGestureRecognizer:(nullable UIScreenEdgePanGestureRecognizer *)gestureRecognizer + viewControllerWillDisappear:(nullable UIViewController *)viewControllerWillDisappear + viewControllerWillAppear:(nullable UIViewController *)viewControllerWillAppear DEPRECATED_MSG_ATTRIBUTE("不便于判断手势返回是否成功,请使用 navigationController:poppingByInteractiveGestureRecognizer:isCancelled:viewControllerWillDisappear:viewControllerWillAppear: 代替"); + +/** + * 当前界面正处于手势返回的过程中,可自行通过 gestureRecognizer.state 来区分手势返回的各个阶段。手势返回有多个阶段(手势返回开始、拖拽过程中、松手并成功返回、松手但不切换界面),不同阶段的 viewController 的状态可能不一样。 + * @param navigationController 当前正在手势返回的 QMUINavigationController,请勿通过 vc.navigationController 获取 UINavigationController 的引用,而应该用本参数。因为某些手势阶段,vc.navigationController 得到的是 nil。 + * @param gestureRecognizer 手势对象 + * @param isCancelled 表示当前手势返回是否取消,只有在松手后这个参数的值才有意义 + * @param viewControllerWillDisappear 手势返回中顶部的那个 vc,松手时如果成功手势返回,则该参数表示被 pop 的界面,如果手势返回取消,则该参数表示背后的界面。 + * @param viewControllerWillAppear 手势返回中背后的那个 vc,松手时如果成功手势返回,则该参数表示背后的界面,如果手势返回取消,则该参数表示当前顶部的界面。 + */ +- (void)navigationController:(nonnull QMUINavigationController *)navigationController +poppingByInteractiveGestureRecognizer:(nullable UIScreenEdgePanGestureRecognizer *)gestureRecognizer + isCancelled:(BOOL)isCancelled + viewControllerWillDisappear:(nullable UIViewController *)viewControllerWillDisappear + viewControllerWillAppear:(nullable UIViewController *)viewControllerWillAppear; /** * 在 self.navigationController 进行以下 4 个操作前,相应的 viewController 的 willPopInNavigationControllerWithAnimated: 方法会被调用: @@ -76,7 +88,9 @@ * 3. popToRootViewControllerAnimated: * 4. setViewControllers:animated: * - * @warning 此时 self 已经不在 viewControllers 数组内 + * 此时 self.navigationController 仍有值,但 self 已经不在 viewControllers 数组内。 + * + * @warning 这个方法被调用并不意味着 self 最终一定会被 pop 掉,例如手势返回被触发时就会调用这个方法,但如果中途取消手势,self 依然会回到 viewControllers 内。 */ - (void)didPopInNavigationControllerWithAnimated:(BOOL)animated; @@ -93,28 +107,26 @@ @optional -/// 是否需要将状态栏改为浅色文字,对于 QMUICommonViewController 子类,返回值默认为宏 StatusbarStyleLightInitially 的值,对于 UIViewController,不实现该方法则视为返回 NO。 -/// @warning 需在项目的 Info.plist 文件内设置字段 “View controller-based status bar appearance” 的值为 NO 才能生效,如果不设置,或者值为 YES,则请使用系统提供的 - preferredStatusBarStyle 方法 -/// 该方法已废弃,请使用系统提供的 - preferredStatusBarStyle 方法。 -- (BOOL)shouldSetStatusBarStyleLight DEPRECATED_ATTRIBUTE; - /// 设置 titleView 的 tintColor -- (nullable UIColor *)titleViewTintColor; +- (nullable UIColor *)qmui_titleViewTintColor; /// 设置导航栏的背景图,默认为 NavBarBackgroundImage -- (nullable UIImage *)navigationBarBackgroundImage; +- (nullable UIImage *)qmui_navigationBarBackgroundImage; /// 设置导航栏底部的分隔线图片,默认为 NavBarShadowImage,必须在 navigationBar 设置了背景图后才有效(系统限制如此) -- (nullable UIImage *)navigationBarShadowImage; +- (nullable UIImage *)qmui_navigationBarShadowImage; /// 设置当前导航栏的 barTintColor,默认为 NavBarBarTintColor -- (nullable UIColor *)navigationBarBarTintColor; +- (nullable UIColor *)qmui_navigationBarBarTintColor; + +/// 设置当前导航栏的 barStyle,默认为 NavBarStyle +- (UIBarStyle)qmui_navigationBarStyle; /// 设置当前导航栏的 UIBarButtonItem 的 tintColor,默认为NavBarTintColor -- (nullable UIColor *)navigationBarTintColor; +- (nullable UIColor *)qmui_navigationBarTintColor; -/// 设置系统返回按钮title,如果返回nil则使用系统默认的返回按钮标题 -- (nullable NSString *)backBarButtonItemTitleWithPreviousViewController:(nullable UIViewController *)viewController; +/// 设置系统返回按钮title,如果返回nil则使用系统默认的返回按钮标题。当实现了这个方法时,会无视配置表 NeedsBackBarButtonItemTitle 的值 +- (nullable NSString *)qmui_backBarButtonItemTitleWithPreviousViewController:(nullable UIViewController *)viewController; @end @@ -144,6 +156,12 @@ */ - (nullable NSString *)customNavigationBarTransitionKey; +/** + * 在实现了系统的自定义转场情况下,导航栏转场的时候是否需要使用 QMUI 自定义的 push / pop transition 效果,默认不实现的话则不会使用,只要前后其中一个 vc 实现并返回了 YES 则会使用。 + * @see UINavigationController+NavigationBarTransition.h + */ +- (BOOL)shouldCustomizeNavigationBarTransitionIfUsingCustomTransitionForOperation:(UINavigationControllerOperation)operation fromViewController:(nullable UIViewController *)fromVC toViewController:(nullable UIViewController *)toVc; + /** * 自定义navBar效果过程中UINavigationController的containerView的背景色 * @see UINavigationController+NavigationBarTransition.h diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUINavigationController.m b/QMUI/QMUIKit/QMUIMainFrame/QMUINavigationController.m index 47a69b4f..bedcc0ac 100644 --- a/QMUI/QMUIKit/QMUIMainFrame/QMUINavigationController.m +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUINavigationController.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUINavigationController.m @@ -19,9 +19,13 @@ #import "QMUICommonViewController.h" #import "UIViewController+QMUI.h" #import "UINavigationController+QMUI.h" +#import "UIView+QMUI.h" +#import "UINavigationItem+QMUI.h" +#import "UINavigationController+QMUI.h" #import "QMUILog.h" #import "QMUIMultipleDelegates.h" #import "QMUIWeakObjectContainer.h" +#import @protocol QMUI_viewWillAppearNotifyDelegate @@ -58,24 +62,47 @@ @implementation UIViewController (QMUINavigationControllerTransition) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - - ExtendImplementationOfVoidMethodWithSingleArgument([UIViewController class], @selector(viewWillAppear:), BOOL, ^(UIViewController *selfObject, BOOL firstArgv) { - if ([selfObject.qmui_viewWillAppearNotifyDelegate respondsToSelector:@selector(qmui_viewControllerDidInvokeViewWillAppear:)]) { - [selfObject.qmui_viewWillAppearNotifyDelegate qmui_viewControllerDidInvokeViewWillAppear:selfObject]; - } + OverrideImplementation([UIViewController class], @selector(viewWillAppear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIViewController *selfObject, BOOL firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if ([selfObject.qmui_viewWillAppearNotifyDelegate respondsToSelector:@selector(qmui_viewControllerDidInvokeViewWillAppear:)]) { + [selfObject.qmui_viewWillAppearNotifyDelegate qmui_viewControllerDidInvokeViewWillAppear:selfObject]; + } + }; }); - ExtendImplementationOfVoidMethodWithSingleArgument([UIViewController class], @selector(viewDidAppear:), BOOL, ^(UIViewController *selfObject, BOOL firstArgv) { - if ([selfObject.navigationController.viewControllers containsObject:selfObject] && [selfObject.navigationController isKindOfClass:[QMUINavigationController class]]) { - ((QMUINavigationController *)selfObject.navigationController).isViewControllerTransiting = NO; - } - selfObject.qmui_poppingByInteractivePopGestureRecognizer = NO; - selfObject.qmui_willAppearByInteractivePopGestureRecognizer = NO; + OverrideImplementation([UIViewController class], @selector(viewDidAppear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIViewController *selfObject, BOOL firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if ([selfObject.navigationController.viewControllers containsObject:selfObject] && [selfObject.navigationController isKindOfClass:[QMUINavigationController class]]) { + ((QMUINavigationController *)selfObject.navigationController).isViewControllerTransiting = NO; + } + selfObject.qmui_poppingByInteractivePopGestureRecognizer = NO; + selfObject.qmui_willAppearByInteractivePopGestureRecognizer = NO; + }; }); - ExtendImplementationOfVoidMethodWithSingleArgument([UIViewController class], @selector(viewDidDisappear:), BOOL, ^(UIViewController *selfObject, BOOL firstArgv) { - selfObject.qmui_poppingByInteractivePopGestureRecognizer = NO; - selfObject.qmui_willAppearByInteractivePopGestureRecognizer = NO; + OverrideImplementation([UIViewController class], @selector(viewDidDisappear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIViewController *selfObject, BOOL firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + selfObject.qmui_poppingByInteractivePopGestureRecognizer = NO; + selfObject.qmui_willAppearByInteractivePopGestureRecognizer = NO; + }; }); }); } @@ -86,8 +113,8 @@ - (void)setQmui_viewWillAppearNotifyDelegate:(id)qmui_viewWillAppearNotifyDelegate { - id weakContainer = objc_getAssociatedObject(self, &kAssociatedObjectKey_qmui_viewWillAppearNotifyDelegate); - if ([weakContainer isKindOfClass:[QMUIWeakObjectContainer class]]) { + QMUIWeakObjectContainer *weakContainer = objc_getAssociatedObject(self, &kAssociatedObjectKey_qmui_viewWillAppearNotifyDelegate); + if (weakContainer.isQMUIWeakObjectContainer) { id notifyDelegate = [weakContainer object]; return notifyDelegate; } @@ -100,32 +127,20 @@ @implementation QMUINavigationController #pragma mark - 生命周期函数 && 基类方法重写 -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { - if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - [self didInitialize]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialize]; - } - return self; -} - -- (void)didInitialize { - +- (void)qmui_didInitialize { + [super qmui_didInitialize]; + self.qmui_alwaysInvokeAppearanceMethods = YES; self.qmui_multipleDelegatesEnabled = YES; self.delegator = [[_QMUINavigationControllerDelegator alloc] init]; self.delegator.navigationController = self; self.delegate = self.delegator; - // UIView.tintColor 并不支持 UIAppearance 协议,所以不能通过 appearance 来设置,只能在实例里设置 - if (QMUICMIActivated) { - self.navigationBar.tintColor = NavBarTintColor; - self.toolbar.tintColor = ToolBarTintColor; - } + BeginIgnoreDeprecatedWarning + [self didInitialize]; + EndIgnoreDeprecatedWarning +} + +- (void)didInitialize { } - (void)dealloc { @@ -157,7 +172,7 @@ - (UIViewController *)popViewControllerAnimated:(BOOL)animated { UIViewController *viewController = [self topViewController]; self.viewControllerPopping = viewController; - + if (animated) { self.viewControllerPopping.qmui_viewWillAppearNotifyDelegate = self; @@ -168,11 +183,11 @@ - (UIViewController *)popViewControllerAnimated:(BOOL)animated { [((UIViewController *)viewController) willPopInNavigationControllerWithAnimated:animated]; } -// QMUILog(@"NavigationItem", @"call popViewControllerAnimated:%@, current viewControllers = %@", StringFromBOOL(animated), self.viewControllers); + // QMUILog(@"NavigationItem", @"call popViewControllerAnimated:%@, current viewControllers = %@", StringFromBOOL(animated), self.viewControllers); viewController = [super popViewControllerAnimated:animated]; -// QMUILog(@"NavigationItem", @"pop viewController: %@", viewController); + // QMUILog(@"NavigationItem", @"pop viewController: %@", viewController); if ([viewController respondsToSelector:@selector(didPopInNavigationControllerWithAnimated:)]) { [((UIViewController *)viewController) didPopInNavigationControllerWithAnimated:animated]; @@ -187,7 +202,7 @@ - (UIViewController *)popViewControllerAnimated:(BOOL)animated { } self.viewControllerPopping = self.topViewController; - + if (animated) { self.viewControllerPopping.qmui_viewWillAppearNotifyDelegate = self; self.isViewControllerTransiting = YES; @@ -232,7 +247,7 @@ - (UIViewController *)popViewControllerAnimated:(BOOL)animated { self.viewControllerPopping.qmui_viewWillAppearNotifyDelegate = self; self.isViewControllerTransiting = YES; } - + // will pop for (NSInteger i = self.viewControllers.count - 1; i > 0; i--) { UIViewController *viewControllerPopping = self.viewControllers[i]; @@ -268,6 +283,11 @@ - (void)setViewControllers:(NSArray *)viewControllers animat } }]; + // setViewControllers 不会触发 pushViewController,所以这里也要更新一下返回按钮的文字 + [viewControllers enumerateObjectsUsingBlock:^(UIViewController * _Nonnull viewController, NSUInteger idx, BOOL * _Nonnull stop) { + [self updateBackItemTitleWithCurrentViewController:viewController nextViewController:idx + 1 < viewControllers.count ? viewControllers[idx + 1] : nil]; + }]; + [super setViewControllers:viewControllers animated:animated]; // did pop @@ -287,33 +307,21 @@ - (void)setViewControllers:(NSArray *)viewControllers animat } - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated { - if (self.isViewControllerTransiting || !viewController) { - QMUILogWarn(NSStringFromClass(self.class), @"%@, 上一次界面切换的动画尚未结束就试图进行新的 push 操作,为了避免产生 bug,拦截了这次 push。\n%s, isViewControllerTransiting = %@, viewController = %@, self.viewControllers = %@", NSStringFromClass(self.class), __func__, StringFromBOOL(self.isViewControllerTransiting), viewController, self.viewControllers); - return; - } + if (!viewController) return; - // 增加一个 presentedViewController 作为判断条件是因为这个 issue:https://github.com/Tencent/QMUI_iOS/issues/261 - if (!self.presentedViewController && animated) { - self.isViewControllerTransiting = YES; + if (self.isViewControllerTransiting && animated) { + QMUILogWarn(NSStringFromClass(self.class), @"%@, 上一次界面切换的动画尚未结束就试图进行新的 push 操作,为了避免产生 bug,将本次 push 改为非动画形式。\n%s, isViewControllerTransiting = %@, viewController = %@, self.viewControllers = %@", NSStringFromClass(self.class), __func__, StringFromBOOL(self.isViewControllerTransiting), viewController, self.viewControllers); + animated = NO; } - if (self.presentedViewController) { - QMUILogWarn(NSStringFromClass(self.class), @"push 的时候 navigationController 存在一个盖在上面的 presentedViewController,可能导致一些 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; } - UIViewController *currentViewController = self.topViewController; - if (currentViewController) { - if (!NeedsBackBarButtonItemTitle) { - // 会自动从 UIBarButtonItem.title 取值作为下一个界面的返回按钮的文字 - currentViewController.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:NULL]; - } else { - UIViewController *vc = (UIViewController *)viewController; - if ([vc respondsToSelector:@selector(backBarButtonItemTitleWithPreviousViewController:)]) { - NSString *title = [vc backBarButtonItemTitleWithPreviousViewController:currentViewController]; - currentViewController.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:nil action:NULL]; - } - } - } + // 在 push 前先设置好返回按钮的文字 + [self updateBackItemTitleWithCurrentViewController:self.topViewController nextViewController:viewController]; [super pushViewController:viewController animated:animated]; @@ -324,6 +332,31 @@ - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)ani } } +- (void)updateBackItemTitleWithCurrentViewController:(UIViewController *)currentViewController nextViewController:(UIViewController *)nextViewController { + if (!currentViewController) return; + + // 如果某个 viewController 显式声明了返回按钮的文字,则无视配置表 NeedsBackBarButtonItemTitle 的值 + UIViewController *vc = (UIViewController *)nextViewController; + if ([vc respondsToSelector:@selector(qmui_backBarButtonItemTitleWithPreviousViewController:)]) { + NSString *title = [vc qmui_backBarButtonItemTitleWithPreviousViewController:currentViewController]; + currentViewController.navigationItem.backBarButtonItem = title ? [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:nil action:NULL] : nil; + return; + } + + // 全局屏蔽返回按钮的文字 + if (QMUICMIActivated && !NeedsBackBarButtonItemTitle) { + if (@available(iOS 14.0, *)) { + // 用新 API 来屏蔽返回按钮的文字,才能保证 iOS 14 长按返回按钮时能正确出现 viewController title + currentViewController.navigationItem.backButtonDisplayMode = UINavigationItemBackButtonDisplayModeMinimal; + return; + } + // 业务自己设置的 backBarButtonItem 优先级高于配置表 + if (!currentViewController.navigationItem.backBarButtonItem) { + currentViewController.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:NULL]; + } + } +} + #pragma mark - 自定义方法 - (BOOL)isViewControllerTransiting { @@ -338,8 +371,8 @@ - (BOOL)isViewControllerTransiting { - (void)handleInteractivePopGestureRecognizer:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer { UIGestureRecognizerState state = gestureRecognizer.state; - UIViewController *viewControllerWillDisappear = self.viewControllerPopping; - UIViewController *viewControllerWillAppear = self.topViewController; + UIViewController *viewControllerWillDisappear = [self.transitionCoordinator viewControllerForKey:UITransitionContextFromViewControllerKey]; + UIViewController *viewControllerWillAppear = [self.transitionCoordinator viewControllerForKey:UITransitionContextToViewControllerKey]; viewControllerWillDisappear.qmui_poppingByInteractivePopGestureRecognizer = YES; viewControllerWillDisappear.qmui_willAppearByInteractivePopGestureRecognizer = NO; @@ -359,16 +392,25 @@ - (void)handleInteractivePopGestureRecognizer:(UIScreenEdgePanGestureRecognizer } if (state == UIGestureRecognizerStateEnded) { - if (CGRectGetMinX(self.topViewController.view.superview.frame) < 0) { - // by molice:只是碰巧发现如果是手势返回取消时,不管在哪个位置取消,self.topViewController.view.superview.frame.orgin.x必定是-112,所以用这个<0的条件来判断 - QMUILog(NSStringFromClass(self.class), @"手势返回放弃了"); - viewControllerWillDisappear = self.topViewController; - viewControllerWillAppear = self.viewControllerPopping; + if (self.transitionCoordinator.cancelled) { + QMUILog(NSStringFromClass(self.class), @"interactivePopGestureRecognizer canceled"); + UIViewController *temp = viewControllerWillDisappear; + viewControllerWillDisappear = viewControllerWillAppear; + viewControllerWillAppear = temp; } else { - QMUILog(NSStringFromClass(self.class), @"执行手势返回"); + QMUILog(NSStringFromClass(self.class), @"interactivePopGestureRecognizer triggered"); } } + if ([viewControllerWillDisappear respondsToSelector:@selector(navigationController:poppingByInteractiveGestureRecognizer:isCancelled:viewControllerWillDisappear:viewControllerWillAppear:)]) { + [((UIViewController *)viewControllerWillDisappear) navigationController:self poppingByInteractiveGestureRecognizer:gestureRecognizer isCancelled:self.transitionCoordinator.cancelled viewControllerWillDisappear:viewControllerWillDisappear viewControllerWillAppear:viewControllerWillAppear]; + } + + if ([viewControllerWillAppear respondsToSelector:@selector(navigationController:poppingByInteractiveGestureRecognizer:isCancelled:viewControllerWillDisappear:viewControllerWillAppear:)]) { + [((UIViewController *)viewControllerWillAppear) navigationController:self poppingByInteractiveGestureRecognizer:gestureRecognizer isCancelled:self.transitionCoordinator.cancelled viewControllerWillDisappear:viewControllerWillDisappear viewControllerWillAppear:viewControllerWillAppear]; + } + + BeginIgnoreDeprecatedWarning if ([viewControllerWillDisappear respondsToSelector:@selector(navigationController:poppingByInteractiveGestureRecognizer:viewControllerWillDisappear:viewControllerWillAppear:)]) { [((UIViewController *)viewControllerWillDisappear) navigationController:self poppingByInteractiveGestureRecognizer:gestureRecognizer viewControllerWillDisappear:viewControllerWillDisappear viewControllerWillAppear:viewControllerWillAppear]; } @@ -376,6 +418,7 @@ - (void)handleInteractivePopGestureRecognizer:(UIScreenEdgePanGestureRecognizer if ([viewControllerWillAppear respondsToSelector:@selector(navigationController:poppingByInteractiveGestureRecognizer:viewControllerWillDisappear:viewControllerWillAppear:)]) { [((UIViewController *)viewControllerWillAppear) navigationController:self poppingByInteractiveGestureRecognizer:gestureRecognizer viewControllerWillDisappear:viewControllerWillDisappear viewControllerWillAppear:viewControllerWillAppear]; } + EndIgnoreDeprecatedWarning } - (void)qmui_viewControllerDidInvokeViewWillAppear:(UIViewController *)viewController { @@ -387,12 +430,67 @@ - (void)qmui_viewControllerDidInvokeViewWillAppear:(UIViewController *)viewContr #pragma mark - StatusBar +- (UIViewController *)childViewControllerIfSearching:(UIViewController *)childViewController customBlock:(BOOL (^)(UIViewController *vc))hasCustomizedStatusBarBlock { + + UIViewController *presentedViewController = childViewController.presentedViewController; + + // 3. 命中这个条件意味着 viewControllers 里某个 vc 被设置了 definesPresentationContext = YES 并 present 了一个 vc(最常见的是进入搜索状态的 UISearchController),此时对 self 而言是不存在 presentedViewController 的,所以在上面第1步里无法得到这个被 present 起来的 vc,也就无法将 statusBar 的控制权交给它,所以这里要特殊处理一下,保证状态栏正确交给 present 起来的 vc + if (!presentedViewController.beingDismissed && presentedViewController && presentedViewController != self.presentedViewController && hasCustomizedStatusBarBlock(presentedViewController)) { + return [self childViewControllerIfSearching:childViewController.presentedViewController customBlock:hasCustomizedStatusBarBlock]; + } + + // 4. 普通 dismiss,或者 iOS 13 默认的半屏 present 手势拖拽下来过程中,或者 UISearchController 退出搜索状态时,都会触发 statusBar 样式刷新,此时的 childViewController 依然是被 dismiss 的那个 vc,但状态栏应该交给背后的界面去控制,所以这里做个保护。为什么需要递归再查一次,是因为 self.topViewController 也可能正在显示一个 present 起来的搜索界面。 + if (childViewController.beingDismissed) { + return [self childViewControllerIfSearching:self.topViewController customBlock:hasCustomizedStatusBarBlock]; + } + + return childViewController; +} + +// 参数 hasCustomizedStatusBarBlock 用于判断指定 vc 是否有自己控制状态栏 hidden/style 的实现。 +- (UIViewController *)childViewControllerForStatusBarWithCustomBlock:(BOOL (^)(UIViewController *vc))hasCustomizedStatusBarBlock { + // 1. 有 modal present 则优先交给 modal present 的 vc 控制(例如进入搜索状态且没指定 definesPresentationContext 的 UISearchController) + UIViewController *childViewController = self.visibleViewController; + + // 2. 如果 modal present 是一个 UINavigationController,则 self.visibleViewController 拿到的是该 UINavigationController.topViewController,而不是该 UINavigationController 本身,所以这里要特殊处理一下,才能让下文的 beingDismissed 判断生效 + if (childViewController.navigationController && (self.presentedViewController == childViewController.navigationController)) { + childViewController = childViewController.navigationController; + } + + childViewController = [self childViewControllerIfSearching:childViewController customBlock:hasCustomizedStatusBarBlock]; + + if (QMUICMIActivated) { + if (hasCustomizedStatusBarBlock(childViewController)) { + return childViewController; + } + return nil; + } + return childViewController; +} + - (UIViewController *)childViewControllerForStatusBarHidden { - return self.topViewController; + return [self childViewControllerForStatusBarWithCustomBlock:^BOOL(UIViewController *vc) { + return vc.qmui_prefersStatusBarHiddenBlock || [vc qmui_hasOverrideUIKitMethod:@selector(prefersStatusBarHidden)]; + }]; } - (UIViewController *)childViewControllerForStatusBarStyle { - return self.topViewController; + return [self childViewControllerForStatusBarWithCustomBlock:^BOOL(UIViewController *vc) { + return vc.qmui_preferredStatusBarStyleBlock || [vc qmui_hasOverrideUIKitMethod:@selector(preferredStatusBarStyle)]; + }]; +} + +- (UIStatusBarStyle)preferredStatusBarStyle { + // 按照系统的文档,当 -[UIViewController childViewControllerForStatusBarStyle] 返回值不为 nil 时,会询问返回的 vc 的 preferredStatusBarStyle,只有当返回 nil 时才会询问 self 的 preferredStatusBarStyle,但实测在 iOS 13 默认的半屏 present 或者 UISearchController 进入搜索状态时,即便在 childViewControllerForStatusBarStyle 里返回了正确的 vc,最终依然会来询问 -[self preferredStatusBarStyle],导致样式错误,所以这里做个保护。 + UIViewController *childViewController = [self childViewControllerForStatusBarStyle]; + if (childViewController) { + return [childViewController preferredStatusBarStyle]; + } + + if (QMUICMIActivated) { + return DefaultStatusBarStyle; + } + return [super preferredStatusBarStyle]; } #pragma mark - 屏幕旋转 @@ -402,12 +500,15 @@ - (BOOL)shouldAutorotate { } - (UIInterfaceOrientationMask)supportedInterfaceOrientations { - // fix iOS 9 UIAlertController bug: UIAlertController:supportedInterfaceOrientations was invoked recursively! + // fix UIAlertController:supportedInterfaceOrientations was invoked recursively! + // crash in iOS 9 and show log in iOS 10 and later // https://github.com/Tencent/QMUI_iOS/issues/502 - if (IOS_VERSION_NUMBER < 100000 && [self.visibleViewController isKindOfClass:UIAlertController.class]) { - return SupportedOrientationMask; + // https://github.com/Tencent/QMUI_iOS/issues/632 + UIViewController *visibleViewController = self.visibleViewController; + if (!visibleViewController || visibleViewController.isBeingDismissed || [visibleViewController isKindOfClass:UIAlertController.class]) { + visibleViewController = self.topViewController; } - return [self.visibleViewController qmui_hasOverrideUIKitMethod:_cmd] ? [self.visibleViewController supportedInterfaceOrientations] : SupportedOrientationMask; + return [visibleViewController qmui_hasOverrideUIKitMethod:_cmd] ? [visibleViewController supportedInterfaceOrientations] : SupportedOrientationMask; } #pragma mark - HomeIndicator @@ -445,3 +546,107 @@ - (void)navigationController:(QMUINavigationController *)navigationController di } @end + + +// 以下 Category 用于解决三种控制返回按钮的方式的优先级冲突问题 +// https://github.com/Tencent/QMUI_iOS/issues/1130 + +@interface UINavigationItem (QMUIBackBarButtonItemTitle) +@property(nonatomic, strong) UIBarButtonItem *qmuibbbt_backItem; +@end + +@implementation UINavigationItem (QMUIBackBarButtonItemTitle) + +QMUISynthesizeIdStrongProperty(qmuibbbt_backItem, setQmuibbbt_backItem); + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation([UINavigationItem class], @selector(setBackBarButtonItem:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationItem *selfObject, UIBarButtonItem *backBarButtonItem) { + + UINavigationBar *navigationBar = selfObject.qmui_navigationBar; + UINavigationController *navigationController = selfObject.qmui_navigationController; + if (navigationController) { + if ([navigationBar.items containsObject:selfObject] + && (navigationBar.topItem != selfObject || navigationController.qmui_isPushing || navigationController.qmui_isPopping) + && (!selfObject.qmuibbbt_backItem || selfObject.qmuibbbt_backItem != backBarButtonItem)) { + // 当前 vc 存在子界面,此时要修改 backBarButtonItem,根据优先级,应该先判断子界面是否使用了 qmui_backBarButtonItemTitleWithPreviousViewController: + UIViewController *currentViewController = nil; + UIViewController *nextViewController = nil; + NSInteger indexForChildViewController = [navigationBar.items indexOfObject:selfObject] + 1; + if (indexForChildViewController < navigationController.viewControllers.count) { + nextViewController = navigationController.viewControllers[indexForChildViewController]; + currentViewController = navigationController.viewControllers[indexForChildViewController - 1]; + } else if (navigationController.qmui_isPopping) { + // 当 UINavigationController 正在 pop 时,navigationBar.items 里仍包含即将被 pop 的界面,但 navigationController.viewControllers 里已经是 pop 结束后的界面了,所以需要从 transitionCoordinator 里获取即将被 pop 的界面 + nextViewController = [navigationController.transitionCoordinator viewControllerForKey:UITransitionContextFromViewControllerKey]; + currentViewController = [navigationController.transitionCoordinator viewControllerForKey:UITransitionContextToViewControllerKey]; + } + if ([nextViewController respondsToSelector:@selector(qmui_backBarButtonItemTitleWithPreviousViewController:)]) { + QMUIAssert(!!currentViewController, @"UINavigationItem (QMUIBackBarButtonItemTitle)", @"currentViewController 和 nextViewController 必须同时存在"); + selfObject.qmuibbbt_backItem = backBarButtonItem; + return; + } else if (!nextViewController) { + QMUILogWarn(@"UINavigationItem (QMUIBackBarButtonItemTitle)", @"当前界面理应存在子界面,但获取不到,qmui_isPopping = %@, navigationBar.items = %@", StringFromBOOL(navigationController.qmui_isPopping), navigationBar.items); + } + } + } + + if (selfObject.qmuibbbt_backItem) { + selfObject.qmuibbbt_backItem = nil; + } + + // call super + void (*originSelectorIMP)(id, SEL, UIBarButtonItem *); + originSelectorIMP = (void (*)(id, SEL, UIBarButtonItem *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, backBarButtonItem); + }; + }); + + OverrideImplementation([UIViewController class], @selector(viewDidAppear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIViewController *selfObject, BOOL firstArgv) { + + // 恢复被屏蔽的那一次 setBackBarButtonItem + if (selfObject.navigationItem.qmuibbbt_backItem) { + selfObject.navigationItem.backBarButtonItem = selfObject.navigationItem.qmuibbbt_backItem; + } + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + }; + }); + }); +} + +@end + +@implementation QMUINavigationTitleView (QMUINavigationController) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // 在先设置了 title 再设置 titleView 时,保证 titleView 的样式能正确。 + OverrideImplementation([UINavigationItem class], @selector(setTitleView:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationItem *selfObject, UIView *titleView) { + + // call super + void (*originSelectorIMP)(id, SEL, UIView *); + originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, titleView); + + if (titleView.qmui_useAsNavigationTitleView) { + if ([selfObject.qmui_viewController respondsToSelector:@selector(qmui_titleViewTintColor)]) { + titleView.tintColor = ((id)selfObject.qmui_viewController).qmui_titleViewTintColor; + } else if (QMUICMIActivated) { + titleView.tintColor = NavBarTitleColor; + } + } + }; + }); + }); +} + +@end diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUITabBarViewController.h b/QMUI/QMUIKit/QMUIMainFrame/QMUITabBarViewController.h index 12c39f27..948b5210 100644 --- a/QMUI/QMUIKit/QMUIMainFrame/QMUITabBarViewController.h +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUITabBarViewController.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITabBarViewController.h @@ -24,7 +24,7 @@ * * Inherent your tabBarController from this, so you can enjoy: * 1. a tabBar with styles defined in configuration templates - *. 2. a tabBar that manages supported interface orientations + * 2. a tabBar that manages supported interface orientations * */ @interface QMUITabBarViewController : UITabBarController diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUITabBarViewController.m b/QMUI/QMUIKit/QMUIMainFrame/QMUITabBarViewController.m index ba98a308..25848e6c 100644 --- a/QMUI/QMUIKit/QMUIMainFrame/QMUITabBarViewController.m +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUITabBarViewController.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // QMUITabBarViewController.m @@ -34,20 +34,30 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { } - (void)didInitialize { - // UIView.tintColor 并不支持 UIAppearance 协议,所以不能通过 appearance 来设置,只能在实例里设置 - if (QMUICMIActivated) { - self.tabBar.tintColor = TabBarTintColor; - } + // subclass hooking } #pragma mark - StatusBar -- (UIViewController *)childViewControllerForStatusBarHidden { - return self.selectedViewController; +// 如果 childViewController 有声明自己的状态栏样式,则用 childViewController 的,否则用 -[QMUITabBarViewController preferredStatusBarStyle] 里的 +- (UIViewController *)childViewControllerForStatusBarStyle { + UIViewController *childViewController = [super childViewControllerForStatusBarStyle]; + if (QMUICMIActivated) { + BOOL hasOverride = childViewController.qmui_preferredStatusBarStyleBlock || [childViewController qmui_hasOverrideUIKitMethod:@selector(preferredStatusBarStyle)]; + if (hasOverride) { + return childViewController; + } + return nil; + } + return childViewController; } -- (UIViewController *)childViewControllerForStatusBarStyle { - return self.selectedViewController; +// 只有 childViewController 没声明自己的状态栏样式时才会走到这里 +- (UIStatusBarStyle)preferredStatusBarStyle { + if (QMUICMIActivated) { + return DefaultStatusBarStyle; + } + return [super preferredStatusBarStyle]; } #pragma mark - 屏幕旋转 @@ -58,14 +68,20 @@ - (BOOL)shouldAutorotate { - (UIInterfaceOrientationMask)supportedInterfaceOrientations { - // fix iOS 9 UIAlertController bug: UIAlertController:supportedInterfaceOrientations was invoked recursively! + // fix UIAlertController:supportedInterfaceOrientations was invoked recursively! + // crash in iOS 9 and show log in iOS 10 and later // https://github.com/Tencent/QMUI_iOS/issues/502 - UIViewController *presentedViewController = self.presentedViewController; - if (IOS_VERSION_NUMBER < 100000 && presentedViewController && [presentedViewController isKindOfClass:UIAlertController.class]) { - presentedViewController = nil; + // https://github.com/Tencent/QMUI_iOS/issues/632 + UIViewController *visibleViewController = self.presentedViewController; + if (!visibleViewController || visibleViewController.isBeingDismissed || [visibleViewController isKindOfClass:UIAlertController.class]) { + visibleViewController = self.selectedViewController; + } + + if ([visibleViewController isKindOfClass:NSClassFromString([NSString stringWithFormat:@"%@%@", @"AV", @"FullScreenViewController"])]) { + return visibleViewController.supportedInterfaceOrientations; } - return presentedViewController ? [presentedViewController supportedInterfaceOrientations] : ([self.selectedViewController qmui_hasOverrideUIKitMethod:_cmd] ? [self.selectedViewController supportedInterfaceOrientations] : SupportedOrientationMask); + return [visibleViewController qmui_hasOverrideUIKitMethod:_cmd] ? [visibleViewController supportedInterfaceOrientations] : SupportedOrientationMask; } #pragma mark - HomeIndicator diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16.imageset/Contents.json new file mode 100644 index 00000000..048a0ef7 --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "QMUI_checkbox16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16.imageset/QMUI_checkbox16.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16.imageset/QMUI_checkbox16.pdf new file mode 100644 index 00000000..901fb7ef Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16.imageset/QMUI_checkbox16.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_checked.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_checked.imageset/Contents.json new file mode 100644 index 00000000..b7519be4 --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_checked.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "QMUI_checkbox16_checked.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_checked.imageset/QMUI_checkbox16_checked.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_checked.imageset/QMUI_checkbox16_checked.pdf new file mode 100644 index 00000000..159ab67b Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_checked.imageset/QMUI_checkbox16_checked.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_disabled.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_disabled.imageset/Contents.json new file mode 100644 index 00000000..651d2cff --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_disabled.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "QMUI_checkbox16_disabled.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_disabled.imageset/QMUI_checkbox16_disabled.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_disabled.imageset/QMUI_checkbox16_disabled.pdf new file mode 100644 index 00000000..ba38d674 Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_disabled.imageset/QMUI_checkbox16_disabled.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_indeterminate.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_indeterminate.imageset/Contents.json new file mode 100644 index 00000000..98c84032 --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_indeterminate.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "QMUI_checkbox16_indeterminate.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_indeterminate.imageset/QMUI_checkbox16_indeterminate.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_indeterminate.imageset/QMUI_checkbox16_indeterminate.pdf new file mode 100644 index 00000000..31d6ea37 Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_indeterminate.imageset/QMUI_checkbox16_indeterminate.pdf differ diff --git a/qmuidemo/Images.xcassets/icon_grid_tabBarItem.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_clear.imageset/Contents.json similarity index 72% rename from qmuidemo/Images.xcassets/icon_grid_tabBarItem.imageset/Contents.json rename to QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_clear.imageset/Contents.json index e08bcab3..83b1bb8f 100644 --- a/qmuidemo/Images.xcassets/icon_grid_tabBarItem.imageset/Contents.json +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_clear.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "icon_grid_tabBarItem.pdf" + "filename" : "QMUI_console_clear.pdf" } ], "info" : { diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_clear.imageset/QMUI_console_clear.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_clear.imageset/QMUI_console_clear.pdf new file mode 100644 index 00000000..9dd541ad Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_clear.imageset/QMUI_console_clear.pdf differ diff --git a/qmuidemo/Images.xcassets/navigationbar_background.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter.imageset/Contents.json similarity index 71% rename from qmuidemo/Images.xcassets/navigationbar_background.imageset/Contents.json rename to QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter.imageset/Contents.json index f382e290..d2770a5e 100644 --- a/qmuidemo/Images.xcassets/navigationbar_background.imageset/Contents.json +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "navigationbar_background.pdf" + "filename" : "QMUI_console_filter.pdf" } ], "info" : { diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter.imageset/QMUI_console_filter.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter.imageset/QMUI_console_filter.pdf new file mode 100644 index 00000000..92875af0 Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter.imageset/QMUI_console_filter.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter_selected.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter_selected.imageset/Contents.json new file mode 100644 index 00000000..941afcb9 --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter_selected.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_console_filter_selected.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/navigationbar_background.imageset/navigationbar_background.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter_selected.imageset/QMUI_console_filter_selected.pdf similarity index 62% rename from qmuidemo/Images.xcassets/navigationbar_background.imageset/navigationbar_background.pdf rename to QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter_selected.imageset/QMUI_console_filter_selected.pdf index b703c44c..7eeb9266 100644 Binary files a/qmuidemo/Images.xcassets/navigationbar_background.imageset/navigationbar_background.pdf and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter_selected.imageset/QMUI_console_filter_selected.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_logo.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_logo.imageset/Contents.json new file mode 100644 index 00000000..082b55ee --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "QMUI_console_logo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_logo.imageset/QMUI_console_logo.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_logo.imageset/QMUI_console_logo.pdf new file mode 100644 index 00000000..87860e1a Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_logo.imageset/QMUI_console_logo.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_emotion_delete.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_emotion_delete.imageset/Contents.json new file mode 100644 index 00000000..60fe877a --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_emotion_delete.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_emotion_delete.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_emotion_delete.imageset/QMUI_emotion_delete.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_emotion_delete.imageset/QMUI_emotion_delete.pdf new file mode 100644 index 00000000..f2f2b481 Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_emotion_delete.imageset/QMUI_emotion_delete.pdf differ diff --git a/qmuidemo/Images.xcassets/launch_background.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_hiddenAlbum.imageset/Contents.json similarity index 74% rename from qmuidemo/Images.xcassets/launch_background.imageset/Contents.json rename to QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_hiddenAlbum.imageset/Contents.json index 6d88cc86..8253ead1 100644 --- a/qmuidemo/Images.xcassets/launch_background.imageset/Contents.json +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_hiddenAlbum.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "launch_background.pdf" + "filename" : "QMUI_hiddenAlbum.pdf" } ], "info" : { diff --git a/qmuidemo/Images.xcassets/launch_background.imageset/launch_background.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_hiddenAlbum.imageset/QMUI_hiddenAlbum.pdf similarity index 65% rename from qmuidemo/Images.xcassets/launch_background.imageset/launch_background.pdf rename to QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_hiddenAlbum.imageset/QMUI_hiddenAlbum.pdf index a6657d18..3cb89d58 100644 Binary files a/qmuidemo/Images.xcassets/launch_background.imageset/launch_background.pdf and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_hiddenAlbum.imageset/QMUI_hiddenAlbum.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_icloud_download_fault.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_icloud_download_fault.imageset/Contents.json new file mode 100644 index 00000000..a253e2cc --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_icloud_download_fault.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_icloud_download_fault.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_icloud_download_fault.imageset/QMUI_icloud_download_fault.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_icloud_download_fault.imageset/QMUI_icloud_download_fault.pdf new file mode 100644 index 00000000..918a1786 Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_icloud_download_fault.imageset/QMUI_icloud_download_fault.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox.imageset/Contents.json new file mode 100644 index 00000000..6323490e --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_pickerImage_checkbox.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox.imageset/QMUI_pickerImage_checkbox.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox.imageset/QMUI_pickerImage_checkbox.pdf new file mode 100644 index 00000000..ff98e1a5 Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox.imageset/QMUI_pickerImage_checkbox.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox_checked.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox_checked.imageset/Contents.json new file mode 100644 index 00000000..ef8e2df6 --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox_checked.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_pickerImage_checkbox_checked.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox_checked.imageset/QMUI_pickerImage_checkbox_checked.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox_checked.imageset/QMUI_pickerImage_checkbox_checked.pdf new file mode 100644 index 00000000..8d102d90 Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox_checked.imageset/QMUI_pickerImage_checkbox_checked.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_favorite.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_favorite.imageset/Contents.json new file mode 100644 index 00000000..aa66cb67 --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_favorite.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_pickerImage_favorite.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_favorite.imageset/QMUI_pickerImage_favorite.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_favorite.imageset/QMUI_pickerImage_favorite.pdf new file mode 100644 index 00000000..a8e7c75d Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_favorite.imageset/QMUI_pickerImage_favorite.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_video_mark.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_video_mark.imageset/Contents.json new file mode 100644 index 00000000..4f25e876 --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_video_mark.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_pickerImage_video_mark.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_video_mark.imageset/QMUI_pickerImage_video_mark.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_video_mark.imageset/QMUI_pickerImage_video_mark.pdf new file mode 100644 index 00000000..013fe39d Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_video_mark.imageset/QMUI_pickerImage_video_mark.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox.imageset/Contents.json new file mode 100644 index 00000000..b68c1583 --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_previewImage_checkbox.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox.imageset/QMUI_previewImage_checkbox.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox.imageset/QMUI_previewImage_checkbox.pdf new file mode 100644 index 00000000..0cf4f556 Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox.imageset/QMUI_previewImage_checkbox.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox_checked.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox_checked.imageset/Contents.json new file mode 100644 index 00000000..5887a270 --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox_checked.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_previewImage_checkbox_checked.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox_checked.imageset/QMUI_previewImage_checkbox_checked.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox_checked.imageset/QMUI_previewImage_checkbox_checked.pdf new file mode 100644 index 00000000..c1573184 Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox_checked.imageset/QMUI_previewImage_checkbox_checked.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_done.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_done.imageset/Contents.json new file mode 100644 index 00000000..0777fd87 --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_done.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_tips_done.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_done.imageset/QMUI_tips_done.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_done.imageset/QMUI_tips_done.pdf new file mode 100644 index 00000000..06e2170f Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_done.imageset/QMUI_tips_done.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_error.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_error.imageset/Contents.json new file mode 100644 index 00000000..1e81f3b3 --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_error.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_tips_error.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_error.imageset/QMUI_tips_error.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_error.imageset/QMUI_tips_error.pdf new file mode 100644 index 00000000..bcf175b9 Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_error.imageset/QMUI_tips_error.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_info.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_info.imageset/Contents.json new file mode 100644 index 00000000..ac8d452b --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_info.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_tips_info.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_info.imageset/QMUI_tips_info.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_info.imageset/QMUI_tips_info.pdf new file mode 100644 index 00000000..06e88d35 Binary files /dev/null and b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_info.imageset/QMUI_tips_info.pdf differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Info.plist b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Info.plist deleted file mode 100644 index 682aeacb..00000000 --- a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Info.plist +++ /dev/null @@ -1,40 +0,0 @@ - - - - - BuildMachineOSBuild - 14D136 - CFBundleDevelopmentRegion - en - CFBundleIdentifier - GYQMUI.QMUIResources - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - QMUIResources - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - DTCompiler - com.apple.compilers.llvm.clang.1_0 - DTPlatformBuild - 6D2105 - DTPlatformVersion - GM - DTSDKBuild - 14D125 - DTSDKName - macosx10.10 - DTXcode - 0632 - DTXcodeBuild - 6D2105 - NSHumanReadableCopyright - Copyright © 2015年 QMUI Team. All rights reserved. - - diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_clear@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_clear@2x.png deleted file mode 100644 index 9b817d6a..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_clear@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_clear@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_clear@3x.png deleted file mode 100644 index 53542fae..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_clear@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_filter@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_filter@2x.png deleted file mode 100644 index 4fcc9cf1..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_filter@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_filter@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_filter@3x.png deleted file mode 100644 index 0c03922a..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_filter@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_filter_selected@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_filter_selected@2x.png deleted file mode 100644 index cde8b5e8..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_filter_selected@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_filter_selected@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_filter_selected@3x.png deleted file mode 100644 index 925269e3..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_filter_selected@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_logo@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_logo@2x.png deleted file mode 100644 index 00709465..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_logo@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_logo@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_logo@3x.png deleted file mode 100644 index ec7359ab..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_console_logo@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_emotion_delete@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_emotion_delete@2x.png deleted file mode 100644 index 5de7dbbb..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_emotion_delete@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_emotion_delete@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_emotion_delete@3x.png deleted file mode 100644 index 7f879894..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_emotion_delete@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_hiddenAlbum@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_hiddenAlbum@2x.png deleted file mode 100644 index 999e045d..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_hiddenAlbum@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_hiddenAlbum@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_hiddenAlbum@3x.png deleted file mode 100644 index 7156304f..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_hiddenAlbum@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault@2x.png deleted file mode 100644 index 74399e27..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault@3x.png deleted file mode 100644 index 067e7baf..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox@2x.png deleted file mode 100644 index ce02de32..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox@3x.png deleted file mode 100644 index 3a415192..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox_checked@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox_checked@2x.png deleted file mode 100644 index 4fa5b763..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox_checked@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox_checked@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox_checked@3x.png deleted file mode 100644 index 3d078d24..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox_checked@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_favorite@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_favorite@2x.png deleted file mode 100644 index 67d1a15b..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_favorite@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_favorite@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_favorite@3x.png deleted file mode 100644 index bf9028ac..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_favorite@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_video_mark@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_video_mark@2x.png deleted file mode 100644 index 98953575..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_video_mark@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_video_mark@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_video_mark@3x.png deleted file mode 100644 index ab8cd226..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_video_mark@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox@2x.png deleted file mode 100644 index a219a1de..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox@3x.png deleted file mode 100644 index da15213c..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox_checked@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox_checked@2x.png deleted file mode 100644 index bad597c8..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox_checked@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox_checked@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox_checked@3x.png deleted file mode 100644 index 8feefcec..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox_checked@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_done@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_done@2x.png deleted file mode 100644 index 5f67e0e1..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_done@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_done@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_done@3x.png deleted file mode 100644 index a900aaeb..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_done@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_error@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_error@2x.png deleted file mode 100644 index 31dbf788..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_error@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_error@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_error@3x.png deleted file mode 100644 index 3bb23b6f..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_error@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_info@2x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_info@2x.png deleted file mode 100644 index 8630a003..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_info@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_info@3x.png b/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_info@3x.png deleted file mode 100644 index ab0fd685..00000000 Binary files a/QMUI/QMUIKit/QMUIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_info@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIKitExtensions/CALayer+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/CALayer+QMUI.h index cad9313b..6c3b8603 100644 --- a/QMUI/QMUIKit/UIKitExtensions/CALayer+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/CALayer+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // CALayer+QMUI.h @@ -17,29 +17,56 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + typedef NS_OPTIONS (NSUInteger, QMUICornerMask) { QMUILayerMinXMinYCorner = 1U << 0, QMUILayerMaxXMinYCorner = 1U << 1, QMUILayerMinXMaxYCorner = 1U << 2, QMUILayerMaxXMaxYCorner = 1U << 3, + QMUILayerAllCorner = QMUILayerMinXMinYCorner|QMUILayerMaxXMinYCorner|QMUILayerMinXMaxYCorner|QMUILayerMaxXMaxYCorner, }; @interface CALayer (QMUI) +/// 是否为某个 UIView 自带的 layer +@property(nonatomic, assign, readonly) BOOL qmui_isRootLayerOfView; + /// 暂停/恢复当前 layer 上的所有动画 @property(nonatomic, assign) BOOL qmui_pause; /** * 设置四个角是否支持圆角的,iOS11 及以上会调用系统的接口,否则 QMUI 额外实现 - * @warning 如果对应的 layer 有圆角,则请使用 QMUI_Border,否则系统的 border 会被 clip 掉 + * @warning 如果对应的 layer 有圆角,则请使用 QMUIBorder,否则系统的 border 会被 clip 掉 * @warning 使用 qmui 方法,则超出 layer 范围内的内容都会被 clip 掉,系统的则不会 * @warning 如果使用这个接口设置圆角,那么需要获取圆角的值需要用 qmui_originCornerRadius,否则 iOS 11 以下获取到的都是 0 */ -@property(nonatomic, assign) QMUICornerMask qmui_maskedCorners; +@property(nonatomic, assign) QMUICornerMask qmui_maskedCorners DEPRECATED_MSG_ATTRIBUTE("请使用系统的 CALayer.maskedCorners,QMUI 4.4.0 开始不再支持 iOS 10,该属性无意义了,后续会删除。"); /// iOS11 以下 layer 自身的 cornerRadius 一直都是 0,圆角的是通过 mask 做的,qmui_originCornerRadius 保存了当前的圆角 @property(nonatomic, assign, readonly) CGFloat qmui_originCornerRadius; +/** + 支持直接用一个 NSShadow 来设置各种 shadow 样式(其实就是把分散的多个 shadowXxx 接口合并为一个)。不保证样式的锁定(也即如果后续用独立的 shadowXxx 接口修改了样式则会被覆盖)。 + @note 当使用这个接口时,shadowOpacity 会强制设置为1,阴影的半透明请通过修改 NSShadow.shadowColor 颜色里的 alpha 来控制。仅当之前已经设置过 qmui_shadow 的情况下,才可以通过 qmui_shadow = nil 来去除阴影。 + */ +@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; + /** * 把某个 sublayer 移动到当前所有 sublayers 的最后面 * @param sublayer 要被移动的 layer @@ -59,6 +86,13 @@ typedef NS_OPTIONS (NSUInteger, QMUICornerMask) { */ - (void)qmui_removeDefaultAnimations; +/** + * 对 CALayer 执行一些操作,不以动画的形式展示过程(默认情况下修改 CALayer 的属性都会以动画形式展示出来)。 + * @param actionsWithoutAnimation 要执行的操作,可以在里面修改 layer 的属性,例如 frame、backgroundColor 等。 + * @note 如果该 layer 的任何属性修改都不需要动画,也可使用 qmui_removeDefaultAnimations。 + */ ++ (void)qmui_performWithoutAnimation:(void (NS_NOESCAPE ^)(void))actionsWithoutAnimation; + /** * 生成虚线的方法,注意返回的是 CAShapeLayer * @param lineLength 每一段的线宽 @@ -96,3 +130,12 @@ typedef NS_OPTIONS (NSUInteger, QMUICornerMask) { + (CALayer *)qmui_separatorLayerForTableView; @end + +@interface CALayer (QMUI_DynamicColor) + +/// 如果 layer 的 backgroundColor、borderColor、shadowColor 是使用 dynamic color(UIDynamicProviderColor、QMUIThemeColor 等)生成的,则调用这个方法可以重新设置一遍这些属性,从而更新颜色 +/// iOS 13 系统设置里的界面样式变化(Dark Mode),以及 QMUIThemeManager 触发的主题变化,都会自动调用 layer 的这个方法,业务无需关心。 +- (void)qmui_setNeedsUpdateDynamicStyle NS_REQUIRES_SUPER; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/CALayer+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/CALayer+QMUI.m index e09752fc..8eff35fb 100644 --- a/QMUI/QMUIKit/UIKitExtensions/CALayer+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/CALayer+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // CALayer+QMUI.m @@ -17,6 +17,7 @@ #import "UIView+QMUI.h" #import "QMUICore.h" #import "QMUILog.h" +#import "UIColor+QMUI.h" @interface CALayer () @@ -38,24 +39,18 @@ + (void)load { ExtendImplementationOfNonVoidMethodWithoutArguments([CALayer class], @selector(init), CALayer *, ^CALayer *(CALayer *selfObject, CALayer *originReturnValue) { selfObject.qmui_speedBeforePause = selfObject.speed; - selfObject.qmui_maskedCorners = QMUILayerMinXMinYCorner|QMUILayerMaxXMinYCorner|QMUILayerMinXMaxYCorner|QMUILayerMaxXMaxYCorner; + selfObject.qmui_maskedCorners = QMUILayerAllCorner; return originReturnValue; }); OverrideImplementation([CALayer class], @selector(setBounds:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(CALayer *selfObject, CGRect bounds) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - // 对非法的 bounds,Debug 下中 assert,Release 下会将其中的 NaN 改为 0,避免 crash - if (CGRectIsNaN(bounds)) { - QMUILogWarn(@"CALayer (QMUI)", @"%@ setBounds:%@,参数包含 NaN,已被拦截并处理为 0。%@", selfObject, NSStringFromCGRect(bounds), [NSThread callStackSymbols]); - if (QMUICMIActivated && !ShouldPrintQMUIWarnLogToConsole) { - NSAssert(NO, @"CALayer setBounds: 出现 NaN"); - } - if (!IS_DEBUG) { - bounds = CGRectSafeValue(bounds); - } + // 对非法的 bounds,Debug 下中 assert,Release 下会将其中的 NaN 改为 0,避免 crash + if (CGRectIsNaN(bounds)) { + QMUIAssert(NO, @"CALayer (QMUI)", @"%@ setBounds:%@,参数包含 NaN,已被拦截并处理为 0。%@", selfObject, NSStringFromCGRect(bounds), [NSThread callStackSymbols]); + if (!IS_DEBUG) { + bounds = CGRectSafeValue(bounds); } } @@ -69,17 +64,11 @@ + (void)load { OverrideImplementation([CALayer class], @selector(setPosition:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(CALayer *selfObject, CGPoint position) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - // 对非法的 position,Debug 下中 assert,Release 下会将其中的 NaN 改为 0,避免 crash - if (isnan(position.x) || isnan(position.y)) { - QMUILogWarn(@"CALayer (QMUI)", @"%@ setPosition:%@,参数包含 NaN,已被拦截并处理为 0。%@", selfObject, NSStringFromCGPoint(position), [NSThread callStackSymbols]); - if (QMUICMIActivated && !ShouldPrintQMUIWarnLogToConsole) { - NSAssert(NO, @"CALayer setPosition: 出现 NaN"); - } - if (!IS_DEBUG) { - position = CGPointMake(CGFloatSafeValue(position.x), CGFloatSafeValue(position.y)); - } + // 对非法的 position,Debug 下中 assert,Release 下会将其中的 NaN 改为 0,避免 crash + if (isnan(position.x) || isnan(position.y)) { + QMUIAssert(NO, @"CALayer (QMUI)", @"%@ setPosition:%@,参数包含 NaN,已被拦截并处理为 0。%@", selfObject, NSStringFromCGPoint(position), [NSThread callStackSymbols]); + if (!IS_DEBUG) { + position = CGPointMake(CGFloatSafeValue(position.x), CGFloatSafeValue(position.y)); } } @@ -92,28 +81,20 @@ + (void)load { }); } +- (BOOL)qmui_isRootLayerOfView { + return [self.delegate isKindOfClass:[UIView class]] && ((UIView *)self.delegate).layer == self; +} + - (void)qmuilayer_setCornerRadius:(CGFloat)cornerRadius { BOOL cornerRadiusChanged = flat(self.qmui_originCornerRadius) != flat(cornerRadius);// flat 处理,避免浮点精度问题 self.qmui_originCornerRadius = cornerRadius; - if (@available(iOS 11, *)) { - [self qmuilayer_setCornerRadius:cornerRadius]; - } else { - if (self.qmui_maskedCorners && ![self hasFourCornerRadius]) { - [self qmuilayer_setCornerRadius:0]; - } else { - [self qmuilayer_setCornerRadius:cornerRadius]; - } - if (cornerRadiusChanged) { - // 需要刷新mask - [self setNeedsLayout]; - } - } + [self qmuilayer_setCornerRadius:cornerRadius]; if (cornerRadiusChanged) { // 需要刷新border if ([self.delegate respondsToSelector:@selector(layoutSublayersOfLayer:)]) { UIView *view = (UIView *)self.delegate; if (view.qmui_borderPosition > 0 && view.qmui_borderWidth > 0) { - [view layoutSublayersOfLayer:self]; + [view.qmui_borderLayer setNeedsLayout];// 直接调用 layer 的 setNeedsLayout,没有线程限制,如果通过 view 调用则需要在主线程才行 } } } @@ -148,29 +129,13 @@ - (BOOL)qmui_pause { - (void)setQmui_maskedCorners:(QMUICornerMask)qmui_maskedCorners { BOOL maskedCornersChanged = qmui_maskedCorners != self.qmui_maskedCorners; objc_setAssociatedObject(self, &kAssociatedObjectKey_maskedCorners, @(qmui_maskedCorners), OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (@available(iOS 11, *)) { - self.maskedCorners = (CACornerMask)qmui_maskedCorners; - } else { - if (qmui_maskedCorners && ![self hasFourCornerRadius]) { - [self qmuilayer_setCornerRadius:0]; - } - if (maskedCornersChanged) { - // 需要刷新mask - if ([NSThread isMainThread]) { - [self setNeedsLayout]; - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - [self setNeedsLayout]; - }); - } - } - } + self.maskedCorners = (CACornerMask)qmui_maskedCorners; if (maskedCornersChanged) { // 需要刷新border if ([self.delegate respondsToSelector:@selector(layoutSublayersOfLayer:)]) { UIView *view = (UIView *)self.delegate; if (view.qmui_borderPosition > 0 && view.qmui_borderWidth > 0) { - [view layoutSublayersOfLayer:self]; + [view.qmui_borderLayer setNeedsLayout];// 直接调用 layer 的 setNeedsLayout,没有线程限制,如果通过 view 调用则需要在主线程才行 } } } @@ -180,18 +145,100 @@ - (QMUICornerMask)qmui_maskedCorners { return [objc_getAssociatedObject(self, &kAssociatedObjectKey_maskedCorners) unsignedIntegerValue]; } -- (void)qmui_sendSublayerToBack:(CALayer *)sublayer { - if (sublayer.superlayer == self) { - [sublayer removeFromSuperlayer]; - [self insertSublayer:sublayer atIndex:0]; +static char kAssociatedObjectKey_shadow; +- (void)setQmui_shadow:(NSShadow *)shadow { + if (shadow) { + if ([shadow.shadowColor isKindOfClass:UIColor.class]) { + self.shadowColor = ((UIColor *)shadow.shadowColor).CGColor; + } + self.shadowOffset = shadow.shadowOffset; + self.shadowRadius = shadow.shadowBlurRadius; + self.shadowOpacity = 1; + } else if (self.qmui_shadow) { + // 仅当之前已经用 qmui_shadow 设置过阴影时,才支持通过 qmui_shadow = nil 来去除阴影,否则什么都不做。 + self.shadowOpacity = 0; } + objc_setAssociatedObject(self, &kAssociatedObjectKey_shadow, shadow, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -- (void)qmui_bringSublayerToFront:(CALayer *)sublayer { - if (sublayer.superlayer == self) { - [sublayer removeFromSuperlayer]; - [self insertSublayer:sublayer atIndex:(unsigned)self.sublayers.count]; +- (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) { + CALayer *result = [sublayer qmui_layerWithName:name]; + if (result) return result; } + return nil; +} + +- (void)qmui_sendSublayerToBack:(CALayer *)sublayer { + [self insertSublayer:sublayer atIndex:0]; +} + +- (void)qmui_bringSublayerToFront:(CALayer *)sublayer { + [self insertSublayer:sublayer atIndex:(unsigned)self.sublayers.count]; } - (void)qmui_removeDefaultAnimations { @@ -226,7 +273,9 @@ - (void)qmui_removeDefaultAnimations { NSStringFromSelector(@selector(shadowOpacity)): [NSNull null], NSStringFromSelector(@selector(shadowOffset)): [NSNull null], NSStringFromSelector(@selector(shadowRadius)): [NSNull null], - NSStringFromSelector(@selector(shadowPath)): [NSNull null]}.mutableCopy; + NSStringFromSelector(@selector(shadowPath)): [NSNull null], + NSStringFromSelector(@selector(maskedCorners)): [NSNull null], + }.mutableCopy; if ([self isKindOfClass:[CAShapeLayer class]]) { [actions addEntriesFromDictionary:@{NSStringFromSelector(@selector(path)): [NSNull null], @@ -249,6 +298,14 @@ - (void)qmui_removeDefaultAnimations { self.actions = actions; } ++ (void)qmui_performWithoutAnimation:(void (NS_NOESCAPE ^)(void))actionsWithoutAnimation { + if (!actionsWithoutAnimation) return; + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + actionsWithoutAnimation(); + [CATransaction commit]; +} + + (CAShapeLayer *)qmui_separatorDashLayerWithLineLength:(NSInteger)lineLength lineSpacing:(NSInteger)lineSpacing lineWidth:(CGFloat)lineWidth @@ -299,69 +356,120 @@ + (CALayer *)qmui_separatorLayerForTableView { return layer; } -- (BOOL)hasFourCornerRadius { - return (self.qmui_maskedCorners & QMUILayerMinXMinYCorner) == QMUILayerMinXMinYCorner && - (self.qmui_maskedCorners & QMUILayerMaxXMinYCorner) == QMUILayerMaxXMinYCorner && - (self.qmui_maskedCorners & QMUILayerMinXMaxYCorner) == QMUILayerMinXMaxYCorner && - (self.qmui_maskedCorners & QMUILayerMaxXMaxYCorner) == QMUILayerMaxXMaxYCorner; +@end + +@interface CAShapeLayer (QMUI_DynamicColor) + +@property(nonatomic, strong) UIColor *qcl_originalFillColor; +@property(nonatomic, strong) UIColor *qcl_originalStrokeColor; + +@end + +@implementation CAShapeLayer (QMUI_DynamicColor) + +QMUISynthesizeIdStrongProperty(qcl_originalFillColor, setQcl_originalFillColor) +QMUISynthesizeIdStrongProperty(qcl_originalStrokeColor, setQcl_originalStrokeColor) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation([CAShapeLayer class], @selector(setFillColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(CAShapeLayer *selfObject, CGColorRef color) { + + UIColor *originalColor = [(__bridge id)(color) qmui_getBoundObjectForKey:QMUICGColorOriginalColorBindKey]; + selfObject.qcl_originalFillColor = originalColor; + + // call super + void (*originSelectorIMP)(id, SEL, CGColorRef); + originSelectorIMP = (void (*)(id, SEL, CGColorRef))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, color); + }; + }); + + OverrideImplementation([CAShapeLayer class], @selector(setStrokeColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(CAShapeLayer *selfObject, CGColorRef color) { + + UIColor *originalColor = [(__bridge id)(color) qmui_getBoundObjectForKey:QMUICGColorOriginalColorBindKey]; + selfObject.qcl_originalStrokeColor = originalColor; + + // call super + void (*originSelectorIMP)(id, SEL, CGColorRef); + originSelectorIMP = (void (*)(id, SEL, CGColorRef))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, color); + }; + }); + }); +} + +- (void)qmui_setNeedsUpdateDynamicStyle { + [super qmui_setNeedsUpdateDynamicStyle]; + + if (self.qcl_originalFillColor) { + self.fillColor = self.qcl_originalFillColor.CGColor; + } + + if (self.qcl_originalStrokeColor) { + self.strokeColor = self.qcl_originalStrokeColor.CGColor; + } } @end -@interface UIView (QMUI_CornerRadius) +@interface CAGradientLayer (QMUI_DynamicColor) + +@property(nonatomic, strong) NSArray * qcl_originalColors; @end -@implementation UIView (QMUI_CornerRadius) +@implementation CAGradientLayer (QMUI_DynamicColor) -static NSString *kMaskName = @"QMUI_CornerRadius_Mask"; +QMUISynthesizeIdStrongProperty(qcl_originalColors, setQcl_originalColors) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ExtendImplementationOfVoidMethodWithSingleArgument([UIView class], @selector(layoutSublayersOfLayer:), CALayer *, ^(UIView *selfObject, CALayer *layer) { - if (@available(iOS 11, *)) { - } else { - if (selfObject.layer.mask && ![selfObject.layer.mask.name isEqualToString:kMaskName]) { - return; - } - if (selfObject.layer.qmui_maskedCorners) { - if (selfObject.layer.qmui_originCornerRadius <= 0 || [selfObject hasFourCornerRadius]) { - if (selfObject.layer.mask) { - selfObject.layer.mask = nil; - } + OverrideImplementation([CAGradientLayer class], @selector(setColors:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(CAGradientLayer *selfObject, NSArray *colors) { + + + void (*originSelectorIMP)(id, SEL, NSArray *); + originSelectorIMP = (void (*)(id, SEL, NSArray *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, colors); + + + __block BOOL hasDynamicColor = NO; + NSMutableArray *originalColors = [NSMutableArray array]; + [colors enumerateObjectsUsingBlock:^(id color, NSUInteger idx, BOOL * _Nonnull stop) { + UIColor *originalColor = [color qmui_getBoundObjectForKey:QMUICGColorOriginalColorBindKey]; + if (originalColor) { + hasDynamicColor = YES; + [originalColors addObject:originalColor]; } else { - CAShapeLayer *cornerMaskLayer = [CAShapeLayer layer]; - cornerMaskLayer.name = kMaskName; - UIRectCorner rectCorner = 0; - if ((selfObject.layer.qmui_maskedCorners & QMUILayerMinXMinYCorner) == QMUILayerMinXMinYCorner) { - rectCorner |= UIRectCornerTopLeft; - } - if ((selfObject.layer.qmui_maskedCorners & QMUILayerMaxXMinYCorner) == QMUILayerMaxXMinYCorner) { - rectCorner |= UIRectCornerTopRight; - } - if ((selfObject.layer.qmui_maskedCorners & QMUILayerMinXMaxYCorner) == QMUILayerMinXMaxYCorner) { - rectCorner |= UIRectCornerBottomLeft; - } - if ((selfObject.layer.qmui_maskedCorners & QMUILayerMaxXMaxYCorner) == QMUILayerMaxXMaxYCorner) { - rectCorner |= UIRectCornerBottomRight; - } - UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:selfObject.bounds byRoundingCorners:rectCorner cornerRadii:CGSizeMake(selfObject.layer.qmui_originCornerRadius, selfObject.layer.qmui_originCornerRadius)]; - cornerMaskLayer.frame = CGRectMakeWithSize(selfObject.bounds.size); - cornerMaskLayer.path = path.CGPath; - selfObject.layer.mask = cornerMaskLayer; + [originalColors addObject:[UIColor colorWithCGColor:(__bridge CGColorRef _Nonnull)(color)]]; } + }]; + + if (hasDynamicColor) { + selfObject.qcl_originalColors = originalColors; + } else { + selfObject.qcl_originalColors = nil; } - } + + }; }); }); } - -- (BOOL)hasFourCornerRadius { - return (self.layer.qmui_maskedCorners & QMUILayerMinXMinYCorner) == QMUILayerMinXMinYCorner && - (self.layer.qmui_maskedCorners & QMUILayerMaxXMinYCorner) == QMUILayerMaxXMinYCorner && - (self.layer.qmui_maskedCorners & QMUILayerMinXMaxYCorner) == QMUILayerMinXMaxYCorner && - (self.layer.qmui_maskedCorners & QMUILayerMaxXMaxYCorner) == QMUILayerMaxXMaxYCorner; + +- (void)qmui_setNeedsUpdateDynamicStyle { + [super qmui_setNeedsUpdateDynamicStyle]; + + if (self.qcl_originalColors) { + NSMutableArray *colors = [NSMutableArray array]; + [self.qcl_originalColors enumerateObjectsUsingBlock:^(UIColor * _Nonnull color, NSUInteger idx, BOOL * _Nonnull stop) { + [colors addObject:(__bridge id _Nonnull)(color.CGColor)]; + }]; + self.colors = colors; + } } @end diff --git a/QMUI/QMUIKit/UIKitExtensions/NSArray+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSArray+QMUI.h index 57fd9a37..8aaa2f1d 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSArray+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSArray+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSArray+QMUI.h @@ -15,6 +15,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + @interface NSArray (QMUI) /** @@ -23,12 +25,12 @@ @param object 要合并的多个数组 @return 合并完的结果 */ -+ (instancetype)qmui_arrayWithObjects:(id)object, ...; ++ (instancetype)qmui_arrayWithObjects:(ObjectType)object, ...; /** * 将多维数组打平成一维数组再遍历所有子元素 */ -- (void)qmui_enumerateNestedArrayWithBlock:(void (^)(id obj, BOOL *stop))block; +- (void)qmui_enumerateNestedArrayWithBlock:(void (NS_NOESCAPE^)(id obj, BOOL *stop))block; /** * 将多维数组递归转换成 mutable 多维数组 @@ -38,5 +40,23 @@ /** * 过滤数组元素,将 block 返回 YES 的 item 重新组装成一个数组返回 */ -- (NSArray *)qmui_filterWithBlock:(BOOL (^)(ObjectType item))block; +- (NSArray *)qmui_filterWithBlock:(BOOL (NS_NOESCAPE^)(ObjectType item))block; + +/** + 过滤数组元素,将第一个令 block 返回值为 YES 的元素返回,如果不存在则返回 nil + */ +- (ObjectType _Nullable)qmui_firstMatchWithBlock:(BOOL (NS_NOESCAPE^)(ObjectType item))block; + +/** +* 转换数组元素,将每个 item 都经过 block 转换成一遍后返回一个等长的数组。 +*/ +- (NSArray *)qmui_mapWithBlock:(id (NS_NOESCAPE^)(ObjectType item, NSInteger index))block; + +/** +* 转换数组元素,将每个 item 经过 block 转换为另一个元素,如果希望移除该 item,可返回 nil。当所有元素都被移除时,本方法返回空的容器。 +*/ +- (NSArray *)qmui_compactMapWithBlock:(id _Nullable (NS_NOESCAPE^)(ObjectType item))block; + @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/NSArray+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSArray+QMUI.m index fea4b80c..c6d824a9 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSArray+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/NSArray+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSArray+QMUI.m @@ -42,7 +42,7 @@ + (instancetype)qmui_arrayWithObjects:(id)object, ... { return result.copy; } -- (void)qmui_enumerateNestedArrayWithBlock:(void (^)(id, BOOL *))block { +- (void)qmui_enumerateNestedArrayWithBlock:(void (NS_NOESCAPE ^)(id _Nonnull, BOOL *))block { BOOL stop = NO; for (NSInteger i = 0; i < self.count; i++) { id object = self[i]; @@ -69,7 +69,7 @@ - (NSMutableArray *)qmui_mutableCopyNestedArray { return mutableResult; } -- (NSArray *)qmui_filterWithBlock:(BOOL (^)(id))block { +- (NSArray *)qmui_filterWithBlock:(BOOL (NS_NOESCAPE^)(id _Nonnull))block { if (!block) { return self; } @@ -84,4 +84,42 @@ - (NSArray *)qmui_filterWithBlock:(BOOL (^)(id))block { return [result copy]; } +- (id)qmui_firstMatchWithBlock:(BOOL (NS_NOESCAPE^)(id _Nonnull))block { + if (!block) { + return nil; + } + for (id item in self) { + if (block(item)) { + return item; + } + } + return nil; +} + +- (NSArray *)qmui_mapWithBlock:(id (NS_NOESCAPE^)(id item, NSInteger index))block { + if (!block) { + return self; + } + + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:self.count]; + for (NSInteger i = 0; i < self.count; i++) { + [result addObject:block(self[i], i)]; + } + 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/QMUI/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.h index cf8a969c..8d45c49b 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSAttributedString+QMUI.h @@ -15,19 +15,30 @@ #import #import +#import "QMUIHelper.h" +#import "NSString+QMUI.h" + +NS_ASSUME_NONNULL_BEGIN -@interface NSAttributedString (QMUI) +/// 如果某个 NSAttributedString 是通过 +[NSAttributedString qmui_attributedStringWithImage:margins:] 创建的,则该 string 会被添加以这个 name 为 key 的 attribute,值为 NSValue 包裹的 UIEdgeInsets。 +UIKIT_EXTERN NSAttributedStringKey const QMUIImageMarginsAttributeName; + +@interface NSAttributedString (QMUI) /** - * 按照中文 2 个字符、英文 1 个字符的方式来计算文本长度 + * @brief 将指定 image 作为 NSTextAttachment 用以生成一段 NSAttributedString。 + * @note 如果该 image 是由 [UIImage qmui_imageWithAttributedString:] 生成的,则会利用 image 内部关联的 attributes 来试图调整 image 的 y 轴偏移值,以使其与其他文本垂直对齐。 + * @param image 要用的图片 */ -- (NSUInteger)qmui_lengthWhenCountingNonASCIICharacterAsTwo; ++ (instancetype)qmui_attributedStringWithImage:(UIImage *)image; /** - * @brief 创建一个包含图片的 attributedString + * @brief 将指定 image 作为 NSTextAttachment 用以生成一段 NSAttributedString,并利用给定的一整段文字的 attributes 来自动居中 image + * @note 一般情况下我们会将某个 image 作为一串富文本里的某一个部分拼接在一起,为了保证 image、string 垂直对齐,需要根据 font、lineHeight 等信息做一些垂直方向的调整,此时你可以将整段文字的 attributes 传进来,内部根据一定规则帮你计算。 * @param image 要用的图片 + * @param attributes 最终一整段文字的 attributes */ -+ (instancetype)qmui_attributedStringWithImage:(UIImage *)image; ++ (instancetype)qmui_attributedStringWithImage:(UIImage *)image alignByAttributes:(NSDictionary *)attributes; /** * @brief 创建一个包含图片的 attributedString @@ -37,7 +48,19 @@ * @param rightMargin 图片距离右侧内容的间距 * @note leftMargin 和 rightMargin 必须大于或等于 0 */ -+ (instancetype)qmui_attributedStringWithImage:(UIImage *)image baselineOffset:(CGFloat)offset leftMargin:(CGFloat)leftMargin rightMargin:(CGFloat)rightMargin; ++ (instancetype)qmui_attributedStringWithImage:(UIImage *)image baselineOffset:(CGFloat)offset leftMargin:(CGFloat)leftMargin rightMargin:(CGFloat)rightMargin DEPRECATED_MSG_ATTRIBUTE("由于命名、参数不够友好,内部用 baseline 的实现方式也可能影响输入框后续文本的样式,因此本方法废弃,请改为用 qmui_attributedStringWithImage:margins:"); + +/** + * @brief 创建一个包含图片的 attributedString,可通过 margins 调整图片在文本里的位置,上下调整不会影响文本布局,左右调整会在图片和文字之间形成空白区域。 + * 注意该方法返回的 string 里会用 QMUIImageMarginsAttributeName 带上 margins 的值(由 NSValue 包裹的 UIEdgeInsets)。 + * @param image 要用的图片 + * @param margins 图片相对默认位置(baseline)的偏移,其中: + * top > 0 则在图片上方增加空隙,图片会往下 + * top < 0 会将图片往上移动 + * left > 0 会在图片左边增加空隙,图片及后续的文本都往右,不支持负值 + * right > 0 会在图片右边增加空隙,图片后面的文本往右,不支持负值 + */ ++ (instancetype)qmui_attributedStringWithImage:(UIImage *)image margins:(UIEdgeInsets)margins; /** * @brief 创建一个用来占位的空白 attributedString @@ -45,8 +68,57 @@ */ + (instancetype)qmui_attributedStringWithFixedSpace:(CGFloat)width; +/** + 获取当前富文本里的文字水平对齐方式,如果存在多个 paragraphStyle 则以第一个的 alignment 值为准。 + 如果当前文本长度为0或不存在 paragraphStyle 属性,则返回默认的 NSTextAlignmentLeft。 + */ +@property(nonatomic, assign, readonly) NSTextAlignment qmui_textAlignment; + @end @interface NSMutableAttributedString (QMUI) +/** + 通过修改 paragraphStyle 来为当前富文本设置水平对齐方式,若不存在 paragraphStyle 则会帮你创建一个。 + */ +@property(nonatomic, assign) NSTextAlignment qmui_textAlignment; + +/** + 修改当前富文本里的 paragraphStyle 属性,若存在多个不同 paragraphStyle 则每个都会调用一次 block。 + 若不存在 paragraphStyle 则会帮你创建一个,且 range 为整个文本长度。 + */ +- (void)qmui_applyParagraphStyle:(void (^)(NSMutableParagraphStyle *aParagraphStyle, NSRange aRange))block; +@end + +@interface UIImage (QMUI_NSAttributedStringSupports) + +/** + * 将富文本渲染成图片,图片的尺寸与文本大小一致,且只按一行来计算。 + * + * 特别地,对于将 NSAttributedString 用于 UITextView 的场景(例如输入框里@人),UITextView 的特性是当前节点的 attributes 会决定后续继续输入的文本的 attributes,而不管 UITextView 是否主动设置了 font、typingAttributes。对于这种场景,如果不作任何处理,在插入由 UIImage 生成的 NSTextAttachment 后,由于这段 NSTextAttacment 已经不带任何 attributes 了,会导致后续输入的文本都回到系统 UITextView 默认样式(例如字号 12pt),这通常不符合开发者预期。因此通过本方法生成的 UIImage,参数 @c attributedString 对象将会被 copy 后关联在生成的 UIImage 内,假设最终这个 UIImage 通过 [NSAttributedString qmui_attributedStringWithImage:] 转为 NSAttributedString,关联的 attributes 也会被作为这段 NSAttributedString 的 attributes,以保证后续输入的文本样式与 image 保持一致。 + */ ++ (nullable UIImage *)qmui_imageWithAttributedString:(NSAttributedString *)attributedString; + +/** + 如果当前 UIImage 是通过 [UIImage qmui_imageWithAttributedString:] 生成的,则通过这个属性可以获取生成图片时使用的 NSAttributedString。 + */ +@property(nullable, nonatomic, strong, readonly) NSAttributedString *qmui_attributedString; + +/** + 如果当前 UIImage 是通过 [UIImage qmui_imageWithAttributedString:] 生成的,则通过这个属性可以获取生成图片时使用的 NSAttributedString 的 attributes。 + */ +@property(nullable, nonatomic, strong, readonly) NSDictionary *qmui_stringAttributes; @end + +@interface QMUIHelper (NSAttributedStringSupports) + +/** + 利用 image 的 size、attributes 里的 font、lineHeight 综合计算出一个垂直方向上的偏移,令该 image 能在一段富文本里与文字垂直居中(这段富文本的 attributes 与参数 attributes 一致) + @param image 富文本里的 image + @param attributes 整段富文本的 attributes + @return image 的垂直偏移,正值表示向下,负值表示向上。可以将这个值作为 -[NSAttributedString qmui_attributedStringWithImage:margins:] 里的参数 margins.top 的值 + */ ++ (CGFloat)topMarginForAttributedImage:(UIImage *)image attributes:(NSDictionary *)attributes; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.m index 124c3393..96a09334 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSAttributedString+QMUI.m @@ -16,72 +16,45 @@ #import "NSAttributedString+QMUI.h" #import "QMUICore.h" #import "NSString+QMUI.h" +#import "UIImage+QMUI.h" +#import "QMUIStringPrivate.h" + +NSAttributedStringKey const QMUIImageMarginsAttributeName = @"QMUI_attributedImageMargins"; +NSString *const kQMUIImageOriginalAttributedStringKey = @"QMUI_attributedString"; @implementation NSAttributedString (QMUI) -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - - // 类簇对不同的init方法对应不同的私有class,所以要用实例来得到真正的class - OverrideImplementation([[[NSAttributedString alloc] initWithString:@""] class], @selector(initWithString:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^NSAttributedString *(NSAttributedString *selfObject, NSString *str) { - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - str = str ?: @""; - } - - // call super - NSAttributedString *(*originSelectorIMP)(id, SEL, NSString *); - originSelectorIMP = (NSAttributedString * (*)(id, SEL, NSString *))originalIMPProvider(); - NSAttributedString * result = originSelectorIMP(selfObject, originCMD, str); - - return result; - }; - }); - - OverrideImplementation([[[NSAttributedString alloc] initWithString:@"" attributes:nil] class], @selector(initWithString:attributes:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^NSAttributedString *(NSAttributedString *selfObject, NSString *str, NSDictionary *attrs) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - str = str ?: @""; - } - - // call super - NSAttributedString *(*originSelectorIMP)(id, SEL, NSString *, NSDictionary *); - originSelectorIMP = (NSAttributedString *(*)(id, SEL, NSString *, NSDictionary *))originalIMPProvider(); - NSAttributedString *result = originSelectorIMP(selfObject, originCMD, str, attrs); - - return result; - }; - }); - }); ++ (instancetype)qmui_attributedStringWithImage:(UIImage *)image { + return [self qmui_attributedStringWithImage:image alignByAttributes:image.qmui_stringAttributes]; } -- (NSUInteger)qmui_lengthWhenCountingNonASCIICharacterAsTwo { - return self.string.qmui_lengthWhenCountingNonASCIICharacterAsTwo; ++ (instancetype)qmui_attributedStringWithImage:(UIImage *)image alignByAttributes:(NSDictionary *)attributes { + CGFloat marginTop = [QMUIHelper topMarginForAttributedImage:image attributes:attributes]; + return [self qmui_attributedStringWithImage:image margins:UIEdgeInsetsMake(marginTop, 0, 0, 0)]; } -+ (instancetype)qmui_attributedStringWithImage:(UIImage *)image { - return [self qmui_attributedStringWithImage:image baselineOffset:0 leftMargin:0 rightMargin:0]; ++ (instancetype)qmui_attributedStringWithImage:(UIImage *)image baselineOffset:(CGFloat)offset leftMargin:(CGFloat)leftMargin rightMargin:(CGFloat)rightMargin { + return [self qmui_attributedStringWithImage:image margins:UIEdgeInsetsMake(-offset, leftMargin, 0, rightMargin)]; } -+ (instancetype)qmui_attributedStringWithImage:(UIImage *)image baselineOffset:(CGFloat)offset leftMargin:(CGFloat)leftMargin rightMargin:(CGFloat)rightMargin { ++ (instancetype)qmui_attributedStringWithImage:(UIImage *)image margins:(UIEdgeInsets)margins { if (!image) { return nil; } NSTextAttachment *attachment = [[NSTextAttachment alloc] init]; attachment.image = image; - attachment.bounds = CGRectMake(0, 0, image.size.width, image.size.height); + attachment.bounds = CGRectMake(0, -margins.top, image.size.width, image.size.height); NSMutableAttributedString *string = [[NSAttributedString attributedStringWithAttachment:attachment] mutableCopy]; - [string addAttribute:NSBaselineOffsetAttributeName value:@(offset) range:NSMakeRange(0, string.length)]; - if (leftMargin > 0) { - [string insertAttributedString:[self qmui_attributedStringWithFixedSpace:leftMargin] atIndex:0]; + if (margins.left > 0) { + [string insertAttributedString:[self qmui_attributedStringWithFixedSpace:margins.left] atIndex:0]; + } + if (margins.right > 0) { + [string appendAttributedString:[self qmui_attributedStringWithFixedSpace:margins.right]]; } - if (rightMargin > 0) { - [string appendAttributedString:[self qmui_attributedStringWithFixedSpace:rightMargin]]; + if (image.qmui_stringAttributes) { + [string addAttributes:image.qmui_stringAttributes range:NSMakeRange(0, string.length)]; } + [string addAttribute:QMUIImageMarginsAttributeName value:[NSValue valueWithUIEdgeInsets:margins] range:NSMakeRange(0, string.length)]; return string; } @@ -92,47 +65,155 @@ + (instancetype)qmui_attributedStringWithFixedSpace:(CGFloat)width { return [self qmui_attributedStringWithImage:image]; } +- (NSTextAlignment)qmui_textAlignment { + if (!self.length) return NSTextAlignmentLeft; + NSParagraphStyle *p = [self attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:nil]; + if (!p) return NSTextAlignmentLeft; + NSTextAlignment alignment = p.alignment; + return alignment; +} + +#pragma mark - + +- (NSUInteger)qmui_lengthWhenCountingNonASCIICharacterAsTwo { + return self.string.qmui_lengthWhenCountingNonASCIICharacterAsTwo; +} + +- (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { + return [QMUIStringPrivate substring:self avoidBreakingUpCharacterSequencesFromIndex:index lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; +} + +- (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index { + return [self qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:index lessValue:YES countingNonASCIICharacterAsTwo:NO]; +} + +- (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { + return [QMUIStringPrivate substring:self avoidBreakingUpCharacterSequencesToIndex:index lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; +} + +- (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index { + return [self qmui_substringAvoidBreakingUpCharacterSequencesToIndex:index lessValue:YES countingNonASCIICharacterAsTwo:NO]; +} + +- (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesWithRange:(NSRange)range lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { + return [QMUIStringPrivate substring:self avoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; +} + +- (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesWithRange:(NSRange)range { + return [self qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:YES countingNonASCIICharacterAsTwo:NO]; +} + +- (instancetype)qmui_stringByRemoveCharacterAtIndex:(NSUInteger)index { + return [QMUIStringPrivate string:self avoidBreakingUpCharacterSequencesByRemoveCharacterAtIndex:index]; +} + +- (instancetype)qmui_stringByRemoveLastCharacter { + return [self qmui_stringByRemoveCharacterAtIndex:self.length - 1]; +} + @end @implementation NSMutableAttributedString (QMUI) +- (void)qmui_applyParagraphStyle:(void (^)(NSMutableParagraphStyle * _Nonnull, NSRange))block { + if (!self.length || !block) return; + __block BOOL applied = NO; + [self enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, self.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSParagraphStyle * _Nullable value, NSRange range, BOOL * _Nonnull stop) { + applied = YES; + NSMutableParagraphStyle *p = value.mutableCopy; + block(p, range); + [self addAttribute:NSParagraphStyleAttributeName value:p.copy range:range]; + }]; + if (!applied) { + NSMutableParagraphStyle *p = NSMutableParagraphStyle.new; + NSRange range = NSMakeRange(0, self.length); + block(p, range); + [self addAttribute:NSParagraphStyleAttributeName value:p.copy range:range]; + } +} + + +- (void)setQmui_textAlignment:(NSTextAlignment)qmui_textAlignment { + [self qmui_applyParagraphStyle:^(NSMutableParagraphStyle * _Nonnull aParagraphStyle, NSRange aRange) { + aParagraphStyle.alignment = qmui_textAlignment; + }]; +} + +@end + +@implementation UIImage (QMUI_NSAttributedStringSupports) + + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - // 类簇对不同的init方法对应不同的私有class,所以要用实例来得到真正的class - OverrideImplementation([[[NSMutableAttributedString alloc] initWithString:@""] class], @selector(initWithString:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^NSMutableAttributedString *(NSMutableAttributedString *selfObject, NSString *str) { - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - str = str ?: @""; - } + OverrideImplementation([UIImage class], @selector(copyWithZone:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^id (UIImage *selfObject, NSZone *firstArgv) { // call super - NSMutableAttributedString *(*originSelectorIMP)(id, SEL, NSString *); - originSelectorIMP = (NSMutableAttributedString *(*)(id, SEL, NSString *))originalIMPProvider(); - NSMutableAttributedString *result = originSelectorIMP(selfObject, originCMD, str); + id (*originSelectorIMP)(id, SEL, NSZone *); + originSelectorIMP = (id (*)(id, SEL, NSZone *))originalIMPProvider(); + id result = originSelectorIMP(selfObject, originCMD, firstArgv); - return result; - }; - }); - - OverrideImplementation([[[NSMutableAttributedString alloc] initWithString:@"" attributes:nil] class], @selector(initWithString:attributes:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^NSMutableAttributedString *(NSMutableAttributedString *selfObject, NSString *str, NSDictionary *attrs) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - str = str ?: @""; + if ([result isKindOfClass:UIImage.class]) { + id obj = [result qmui_getBoundObjectForKey:kQMUIImageOriginalAttributedStringKey]; + if (obj) { + [result qmui_bindObjectWeakly:obj forKey:kQMUIImageOriginalAttributedStringKey]; + } } - - // call super - NSMutableAttributedString *(*originSelectorIMP)(id, SEL, NSString *, NSDictionary *); - originSelectorIMP = (NSMutableAttributedString *(*)(id, SEL, NSString *, NSDictionary *))originalIMPProvider(); - NSMutableAttributedString *result = originSelectorIMP(selfObject, originCMD, str, attrs); - return result; }; }); }); } ++ (UIImage *)qmui_imageWithAttributedString:(NSAttributedString *)attributedString { + CGSize stringSize = [attributedString boundingRectWithSize:CGSizeMax options:NSStringDrawingUsesLineFragmentOrigin context:nil].size; + stringSize = CGSizeCeil(stringSize); + UIImage *image = [UIImage qmui_imageWithSize:stringSize opaque:NO scale:0 actions:^(CGContextRef contextRef) { + [attributedString drawInRect:CGRectMakeWithSize(stringSize)]; + }]; + [image qmui_bindObject:attributedString.copy forKey:kQMUIImageOriginalAttributedStringKey]; + return image; +} + +- (NSAttributedString *)qmui_attributedString { + return [self qmui_getBoundObjectForKey:kQMUIImageOriginalAttributedStringKey]; +} + +- (NSDictionary *)qmui_stringAttributes { + NSAttributedString *string = self.qmui_attributedString; + NSRange range = NSMakeRange(0, string.length); + return [[self qmui_attributedString] attributesAtIndex:0 effectiveRange:&range]; +} + +@end + +@implementation QMUIHelper (NSAttributedStringSupports) + ++ (CGFloat)topMarginForAttributedImage:(UIImage *)image attributes:(NSDictionary *)attributes { + if (!image || !attributes) return 0; + + CGFloat marginTop = 0; + CGFloat fontCapHeight = ({ + UIFont *font = attributes[NSFontAttributeName]; + font ? font.capHeight : 0; + }); + CGFloat fontLineHeight = ({ + UIFont *font = attributes[NSFontAttributeName]; + font ? font.lineHeight : 0; + }); + CGFloat lineHeight = ({ + NSParagraphStyle *paragraphStyle = attributes[NSParagraphStyleAttributeName]; + paragraphStyle ? paragraphStyle.maximumLineHeight : 0; + }); + CGFloat imageHeight = image.size.height; + if (fontCapHeight) { + marginTop = -(fontCapHeight - imageHeight) / 2; + } + if (fontLineHeight && lineHeight) { + marginTop -= (lineHeight - fontLineHeight) / 2; + } + return marginTop; +} + @end diff --git a/QMUI/QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.h index 347788e0..78229312 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSCharacterSet+QMUI.h diff --git a/QMUI/QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.m index 66cb23d0..0a2ea0ff 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSCharacterSet+QMUI.m diff --git a/QMUI/QMUIKit/UIKitExtensions/NSDictionary+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSDictionary+QMUI.h new file mode 100644 index 00000000..74e6d175 --- /dev/null +++ b/QMUI/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/QMUI/QMUIKit/UIKitExtensions/NSDictionary+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSDictionary+QMUI.m new file mode 100644 index 00000000..72ff1974 --- /dev/null +++ b/QMUI/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/QMUI/QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.h index 9e5aed67..8f0d779c 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.h @@ -1,9 +1,15 @@ +/** + * 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. + */ // // NSMethodSignature+QMUI.h // QMUIKit // // Created by MoLice on 2019/A/28. -// Copyright © 2019 QMUI Team. All rights reserved. // #import @@ -12,6 +18,11 @@ NS_ASSUME_NONNULL_BEGIN @interface NSMethodSignature (QMUI) +/** + 返回一个避免 crash 的方法签名,用于重写 methodSignatureForSelector: 时作为垫底的 return 方案 + */ +@property(nullable, class, nonatomic, readonly) NSMethodSignature *qmui_avoidExceptionSignature; + /** 以 NSString 格式返回当前 NSMethodSignature 的 typeEncoding,例如 v@: */ diff --git a/QMUI/QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.m index c1b6654b..7b654935 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.m @@ -1,9 +1,15 @@ +/** + * 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. + */ // // NSMethodSignature+QMUI.m // QMUIKit // // Created by MoLice on 2019/A/28. -// Copyright © 2019 QMUI Team. All rights reserved. // #import "NSMethodSignature+QMUI.h" @@ -12,6 +18,18 @@ @implementation NSMethodSignature (QMUI) ++ (NSMethodSignature *)qmui_avoidExceptionSignature { + // https://github.com/facebookarchive/AsyncDisplayKit/pull/1562 + // Unfortunately, in order to get this object to work properly, the use of a method which creates an NSMethodSignature + // from a C string. -methodSignatureForSelector is called when a compiled definition for the selector cannot be found. + // This is the place where we have to create our own dud NSMethodSignature. This is necessary because if this method + // returns nil, a selector not found exception is raised. The string argument to -signatureWithObjCTypes: outlines + // the return type and arguments to the message. To return a dud NSMethodSignature, pretty much any signature will + // suffice. Since the -forwardInvocation call will do nothing if the delegate does not respond to the selector, + // the dud NSMethodSignature simply gets us around the exception. + return [NSMethodSignature signatureWithObjCTypes:"@^v^c"]; +} + - (NSString *)qmui_typeString { BeginIgnorePerformSelectorLeaksWarning NSString *typeString = [self performSelector:NSSelectorFromString([NSString stringWithFormat:@"_%@String", @"type"])]; diff --git a/QMUI/QMUIKit/UIKitExtensions/NSNumber+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSNumber+QMUI.h index 555916d0..10bb929d 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSNumber+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSNumber+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSNumber+QMUI.h diff --git a/QMUI/QMUIKit/UIKitExtensions/NSNumber+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSNumber+QMUI.m index 42467db9..23838cb9 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSNumber+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/NSNumber+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSNumber+QMUI.m diff --git a/QMUI/QMUIKit/UIKitExtensions/NSObject+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSObject+QMUI.h index 797e9f3a..22334fa1 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSObject+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSObject+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSObject+QMUI.h @@ -16,6 +16,8 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + @interface NSObject (QMUI) /** @@ -43,7 +45,7 @@ @return 消息执行后的结果 @link http://stackoverflow.com/questions/14635024/using-objc-msgsendsuper-to-invoke-a-class-method @/link */ -- (id)qmui_performSelectorToSuperclass:(SEL)aSelector; +- (nullable id)qmui_performSelectorToSuperclass:(SEL)aSelector; /** 对 super 发送消息 @@ -53,7 +55,7 @@ @return 消息执行后的结果 @link http://stackoverflow.com/questions/14635024/using-objc-msgsendsuper-to-invoke-a-class-method @/link */ -- (id)qmui_performSelectorToSuperclass:(SEL)aSelector withObject:(id)object; +- (nullable id)qmui_performSelectorToSuperclass:(SEL)aSelector withObject:(nullable id)object; /** * 调用一个无参数、返回值类型为非对象的 selector。如果返回值类型为对象,请直接使用系统的 performSelector: 方法。 @@ -65,7 +67,7 @@ * [view qmui_performSelector:@selector(alpha) withPrimitiveReturnValue:&alpha]; * @endcode */ -- (void)qmui_performSelector:(SEL)selector withPrimitiveReturnValue:(void *)returnValue; +- (void)qmui_performSelector:(SEL)selector withPrimitiveReturnValue:(nullable void *)returnValue; /** * 调用一个带参数的 selector,参数类型支持对象和非对象,也没有数量限制。返回值为对象或者 void。 @@ -80,7 +82,7 @@ * [control qmui_performSelector:@selector(addTarget:action:forControlEvents:) withArguments:&target, &action, &events, nil]; * @endcode */ -- (id)qmui_performSelector:(SEL)selector withArguments:(void *)firstArgument, ...; +- (nullable id)qmui_performSelector:(SEL)selector withArguments:(nullable void *)firstArgument, ...; /** * 调用一个返回值类型为非对象且带参数的 selector,参数类型支持对象和非对象,也没有数量限制。 @@ -96,7 +98,7 @@ * [view qmui_performSelector:@selector(pointInside:withEvent:) withPrimitiveReturnValue:&isInside arguments:&point, &event, nil]; * @endcode */ -- (void)qmui_performSelector:(SEL)selector withPrimitiveReturnValue:(void *)returnValue arguments:(void *)firstArgument, ...; +- (void)qmui_performSelector:(SEL)selector withPrimitiveReturnValue:(nullable void *)returnValue arguments:(nullable void *)firstArgument, ...; /** @@ -104,7 +106,7 @@ @param block 用于遍历的 block */ -- (void)qmui_enumrateIvarsUsingBlock:(void (^)(Ivar ivar, NSString *ivarName))block; +- (void)qmui_enumrateIvarsUsingBlock:(void (^)(Ivar ivar, NSString *ivarDescription))block; /** 使用 block 遍历指定 class 的所有成员变量(也即 _xxx 那种),不包含 property 对应的 _property 成员变量 @@ -113,7 +115,7 @@ @param includingInherited 是否要包含由继承链带过来的 ivars @param block 用于遍历的 block */ -+ (void)qmui_enumrateIvarsOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(Ivar ivar, NSString *ivarName))block; ++ (void)qmui_enumrateIvarsOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(Ivar ivar, NSString *ivarDescription))block; /** 使用 block 遍历指定 class 的所有属性,不包含 superclasses 里定义的 property @@ -155,6 +157,56 @@ @end +@interface NSObject (QMUI_KeyValueCoding) + +/** + iOS 13 下系统禁止通过 KVC 访问私有 API,因此提供这种方式在遇到 access prohibited 的异常时可以取代 valueForKey: 使用。 + + 对 iOS 12 及以下的版本,等价于 valueForKey:。 + + @note QMUI 提供2种方式兼容系统的 access prohibited 异常: + 1. 通过将配置表的 IgnoreKVCAccessProhibited 置为 YES 来全局屏蔽系统的异常警告,代码中依然正常使用系统的 valueForKey:、setValue:forKey:,当开启后再遇到 access prohibited 异常时,将会用 QMUIWarnLog 来提醒,不再中断 App 的运行,这是首选推荐方案。 + 2. 使用 qmui_valueForKey:、qmui_setValue:forKey: 代替系统的 valueForKey:、setValue:forKey:,适用于不希望全局屏蔽,只针对某个局部代码自己处理的场景。 + + @link https://github.com/Tencent/QMUI_iOS/issues/617 + + @param key ivar 属性名,支持下划线或不带下划线 + @return key 对应的 value,如果该 key 原本是非对象的值,会被用 NSNumber、NSValue 包裹后返回 + */ +- (nullable id)qmui_valueForKey:(NSString *)key; + +/** + iOS 13 下系统禁止通过 KVC 访问私有 API,因此提供这种方式在遇到 access prohibited 的异常时可以取代 setValue:forKey: 使用。 + + 对 iOS 12 及以下的版本,等价于 setValue:forKey:。 + + @note QMUI 提供2种方式兼容系统的 access prohibited 异常: + 1. 通过将配置表的 IgnoreKVCAccessProhibited 置为 YES 来全局屏蔽系统的异常警告,代码中依然正常使用系统的 valueForKey:、setValue:forKey:,当开启后再遇到 access prohibited 异常时,将会用 QMUIWarnLog 来提醒,不再中断 App 的运行,这是首选推荐方案。 + 2. 使用 qmui_valueForKey:、qmui_setValue:forKey: 代替系统的 valueForKey:、setValue:forKey:,适用于不希望全局屏蔽,只针对某个局部代码自己处理的场景。 + + @link https://github.com/Tencent/QMUI_iOS/issues/617 + + @param key ivar 属性名,支持下划线或不带下划线 + @return key 对应的 value,如果该 key 原本是非对象的值,会被用 NSNumber、NSValue 包裹后返回 + */ +- (void)qmui_setValue:(nullable id)value forKey:(NSString *)key; + +/** + 检查给定的 key 是否可以用于当前对象的 valueForKey: 调用。 + + @note 这是针对 valueForKey: 内部查找 key 的逻辑的精简版,去掉了一些不常用的,如果按精简版查找不到,会返回 NO(但按完整版可能是能查找到的),避免抛出异常。文档描述的查找方法完整版请查看 https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueCoding/SearchImplementation.html + */ +- (BOOL)qmui_canGetValueForKey:(NSString *)key; + +/** +检查给定的 key 是否可以用于当前对象的 setValue:forKey: 调用。 + +@note 对于 setter 而言这就是完整版的检查流程,可核对文档 https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueCoding/SearchImplementation.html +*/ +- (BOOL)qmui_canSetValueForKey:(NSString *)key; + +@end + @interface NSObject (QMUI_DataBind) @@ -176,17 +228,17 @@ } @endcode */ -- (void)qmui_bindObject:(id)object forKey:(NSString *)key; +- (void)qmui_bindObject:(nullable id)object forKey:(NSString *)key; /** 给对象绑定上另一个对象以供后续取出使用,但相比于 qmui_bindObject:forKey:,该方法不会 strong 强引用传入的 object */ -- (void)qmui_bindObjectWeakly:(id)object forKey:(NSString *)key; +- (void)qmui_bindObjectWeakly:(nullable id)object forKey:(NSString *)key; /** 取出之前使用 bind 方法绑定的对象 */ -- (id)qmui_getBoundObjectForKey:(NSString *)key; +- (nullable id)qmui_getBoundObjectForKey:(NSString *)key; /** 给对象绑定上一个 double 值以供后续取出使用 @@ -240,3 +292,27 @@ - (BOOL)qmui_hasBindingKey:(NSString *)key; @end + +@interface NSObject (QMUI_Debug) + +/// 获取当前对象的所有 @property、方法,父类的方法也会分别列出 +@property(nonatomic, copy, readonly) NSString *qmui_methodList; + +/// 获取当前对象的所有 @property、方法,不包含父类的 +@property(nonatomic, copy, readonly) NSString *qmui_shortMethodList; + +/// 获取当前对象的所有 Ivar 变量,并在 Ivar 名字前面显示该 Ivar 的 offset,会同时显示十进制和十六进制,以“|”隔开。 +@property(nonatomic, copy, readonly) NSString *qmui_ivarList; + +/// 获取当前 UIView 层级树信息(只对 UIView 有效) +@property(nonatomic, copy, readonly) NSString *qmui_viewInfo; +@end + +@interface NSThread (QMUI_KVC) + +/// 是否将当前线程标记为忽略系统的 KVC access prohibited 警告,默认为 NO,当开启后,NSException 将不会再抛出 access prohibited 异常 +/// @see BeginIgnoreUIKVCAccessProhibited、EndIgnoreUIKVCAccessProhibited +@property(nonatomic, assign) BOOL qmui_shouldIgnoreUIKVCAccessProhibited; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/NSObject+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSObject+QMUI.m index 8969372e..9fd21b90 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSObject+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/NSObject+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSObject+QMUI.m @@ -15,10 +15,14 @@ #import "NSObject+QMUI.h" #import "QMUIWeakObjectContainer.h" +#import "QMUICore.h" +#import "NSString+QMUI.h" #import + @implementation NSObject (QMUI) + - (BOOL)qmui_hasOverrideMethod:(SEL)selector ofSuperclass:(Class)superclass { return [NSObject qmui_hasOverrideMethod:selector forClass:self.class ofSuperclass:superclass]; } @@ -80,7 +84,7 @@ - (id)qmui_performSelector:(SEL)selector withArguments:(void *)firstArgument, .. [invocation invoke]; const char *typeEncoding = method_getTypeEncoding(class_getInstanceMethod(object_getClass(self), selector)); - if (strncmp(typeEncoding, "@", 1) == 0) { + if (isObjectTypeEncoding(typeEncoding)) { __unsafe_unretained id returnValue; [invocation getReturnValue:&returnValue]; return returnValue; @@ -93,7 +97,9 @@ - (void)qmui_performSelector:(SEL)selector withPrimitiveReturnValue:(void *)retu } - (void)qmui_performSelector:(SEL)selector withPrimitiveReturnValue:(void *)returnValue arguments:(void *)firstArgument, ... { - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]]; + NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector]; + QMUIAssert(methodSignature, @"NSObject (QMUI)", @"- [%@ qmui_performSelector:@selector(%@)] 失败,方法不存在。", NSStringFromClass(self.class), NSStringFromSelector(selector)); + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; [invocation setTarget:self]; [invocation setSelector:selector]; @@ -118,32 +124,79 @@ - (void)qmui_performSelector:(SEL)selector withPrimitiveReturnValue:(void *)retu } } -- (void)qmui_enumrateIvarsUsingBlock:(void (^)(Ivar ivar, NSString *ivarName))block { - [NSObject qmui_enumrateIvarsOfClass:self.class includingInherited:NO usingBlock:block]; +- (void)qmui_enumrateIvarsUsingBlock:(void (^)(Ivar ivar, NSString *ivarDescription))block { + [self qmui_enumrateIvarsIncludingInherited:NO usingBlock:block]; } -+ (void)qmui_enumrateIvarsOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(Ivar, NSString *))block { +- (void)qmui_enumrateIvarsIncludingInherited:(BOOL)includingInherited usingBlock:(void (^)(Ivar ivar, NSString *ivarDescription))block { + NSMutableArray *ivarDescriptions = [NSMutableArray new]; + BeginIgnorePerformSelectorLeaksWarning + NSString *ivarList = [self performSelector:NSSelectorFromString(@"_ivarDescription")]; + EndIgnorePerformSelectorLeaksWarning + NSError *error; + NSRegularExpression *reg = [NSRegularExpression regularExpressionWithPattern:[NSString stringWithFormat:@"in %@:(.*?)((?=in \\w+:)|$)", NSStringFromClass(self.class)] options:NSRegularExpressionDotMatchesLineSeparators error:&error]; + if (!error) { + NSArray *result = [reg matchesInString:ivarList options:NSMatchingReportCompletion range:NSMakeRange(0, ivarList.length)]; + [result enumerateObjectsUsingBlock:^(NSTextCheckingResult * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + NSString *ivars = [ivarList substringWithRange:[obj rangeAtIndex:1]]; + [ivars enumerateLinesUsingBlock:^(NSString * _Nonnull line, BOOL * _Nonnull stop) { + if (![line hasPrefix:@"\t\t"]) {// 有些 struct 类型的变量,会把 struct 的成员也缩进打出来,所以用这种方式过滤掉 + line = line.qmui_trim; + if (line.length > 2) {// 过滤掉空行或者 struct 结尾的"}" + NSRange range = [line rangeOfString:@":"]; + if (range.location != NSNotFound)// 有些"unknow type"的变量不会显示指针地址(例如 UIView->_viewFlags) + line = [line substringToIndex:range.location];// 去掉指针地址 + NSUInteger typeStart = [line rangeOfString:@" ("].location; + line = [NSString stringWithFormat:@"%@ %@", [line substringWithRange:NSMakeRange(typeStart + 2, line.length - 1 - (typeStart + 2))], [line substringToIndex:typeStart]];// 交换变量类型和变量名的位置,变量类型在前,变量名在后,空格隔开 + [ivarDescriptions addObject:line]; + } + } + }]; + }]; + } + unsigned int outCount = 0; - Ivar *ivars = class_copyIvarList(aClass, &outCount); + Ivar *ivars = class_copyIvarList(self.class, &outCount); for (unsigned int i = 0; i < outCount; i ++) { Ivar ivar = ivars[i]; - if (block) block(ivar, [NSString stringWithFormat:@"%s", ivar_getName(ivar)]); + NSString *ivarName = [NSString stringWithFormat:@"%s", ivar_getName(ivar)]; + for (NSString *desc in ivarDescriptions) { + if ([desc hasSuffix:ivarName]) { + block(ivar, desc); + break; + } + } } free(ivars); if (includingInherited) { - Class superclass = class_getSuperclass(aClass); + Class superclass = self.superclass; if (superclass) { [NSObject qmui_enumrateIvarsOfClass:superclass includingInherited:includingInherited usingBlock:block]; } } } ++ (void)qmui_enumrateIvarsOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(Ivar, NSString *))block { + if (!block) return; + NSObject *obj = nil; + if ([aClass isSubclassOfClass:[UICollectionView class]]) { + obj = [[aClass alloc] initWithFrame:CGRectZero collectionViewLayout:UICollectionViewFlowLayout.new]; + } else if ([aClass isSubclassOfClass:[UIApplication class]]) { + obj = UIApplication.sharedApplication; + } else { + obj = [aClass new]; + } + [obj qmui_enumrateIvarsIncludingInherited:includingInherited usingBlock:block]; +} + - (void)qmui_enumratePropertiesUsingBlock:(void (^)(objc_property_t property, NSString *propertyName))block { [NSObject qmui_enumratePropertiesOfClass:self.class includingInherited:NO usingBlock:block]; } + (void)qmui_enumratePropertiesOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(objc_property_t, NSString *))block { + if (!block) return; + unsigned int propertiesCount = 0; objc_property_t *properties = class_copyPropertyList(aClass, &propertiesCount); @@ -167,6 +220,8 @@ - (void)qmui_enumrateInstanceMethodsUsingBlock:(void (^)(Method, SEL))block { } + (void)qmui_enumrateInstanceMethodsOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(Method, SEL))block { + if (!block) return; + unsigned int methodCount = 0; Method *methods = class_copyMethodList(aClass, &methodCount); @@ -187,6 +242,8 @@ + (void)qmui_enumrateInstanceMethodsOfClass:(Class)aClass includingInherited:(BO } + (void)qmui_enumerateProtocolMethods:(Protocol *)protocol usingBlock:(void (^)(SEL))block { + if (!block) return; + unsigned int methodCount = 0; struct objc_method_description *methods = protocol_copyMethodDescriptionList(protocol, NO, YES, &methodCount); for (int i = 0; i < methodCount; i++) { @@ -200,6 +257,80 @@ + (void)qmui_enumerateProtocolMethods:(Protocol *)protocol usingBlock:(void (^)( @end +@implementation NSObject (QMUI_KeyValueCoding) + +- (id)qmui_valueForKey:(NSString *)key { + if ([self isKindOfClass:[UIView class]] && QMUICMIActivated && !IgnoreKVCAccessProhibited) { + BeginIgnoreUIKVCAccessProhibited + id value = [self valueForKey:key]; + EndIgnoreUIKVCAccessProhibited + return value; + } + return [self valueForKey:key]; +} + +- (void)qmui_setValue:(id)value forKey:(NSString *)key { + if ([self isKindOfClass:[UIView class]] && QMUICMIActivated && !IgnoreKVCAccessProhibited) { + BeginIgnoreUIKVCAccessProhibited + [self setValue:value forKey:key]; + EndIgnoreUIKVCAccessProhibited + return; + } + + [self setValue:value forKey:key]; +} + +- (BOOL)qmui_canGetValueForKey:(NSString *)key { + NSArray *getters = @[ + [NSString stringWithFormat:@"get%@", key.qmui_capitalizedString], // get + key, + [NSString stringWithFormat:@"is%@", key.qmui_capitalizedString], // is + [NSString stringWithFormat:@"_%@", key] // _ + ]; + for (NSString *selectorString in getters) { + if ([self respondsToSelector:NSSelectorFromString(selectorString)]) return YES; + } + + if (![self.class accessInstanceVariablesDirectly]) return NO; + + return [self _qmui_hasSpecifiedIvarWithKey:key]; +} + +- (BOOL)qmui_canSetValueForKey:(NSString *)key { + NSArray *setter = @[ + [NSString stringWithFormat:@"set%@:", key.qmui_capitalizedString], // set: + [NSString stringWithFormat:@"_set%@", key.qmui_capitalizedString] // _set + ]; + for (NSString *selectorString in setter) { + if ([self respondsToSelector:NSSelectorFromString(selectorString)]) return YES; + } + + if (![self.class accessInstanceVariablesDirectly]) return NO; + + return [self _qmui_hasSpecifiedIvarWithKey:key]; +} + +- (BOOL)_qmui_hasSpecifiedIvarWithKey:(NSString *)key { + __block BOOL result = NO; + NSArray *ivars = @[ + [NSString stringWithFormat:@"_%@", key], + [NSString stringWithFormat:@"_is%@", key.qmui_capitalizedString], + key, + [NSString stringWithFormat:@"is%@", key.qmui_capitalizedString] + ]; + [NSObject qmui_enumrateIvarsOfClass:self.class includingInherited:YES usingBlock:^(Ivar _Nonnull ivar, NSString * _Nonnull ivarDescription) { + if (!result) { + NSString *ivarName = [NSString stringWithFormat:@"%s", ivar_getName(ivar)]; + if ([ivars containsObject:ivarName]) { + result = YES; + } + } + }]; + return result; +} + +@end + @implementation NSObject (QMUI_DataBind) @@ -215,7 +346,6 @@ @implementation NSObject (QMUI_DataBind) - (void)qmui_bindObject:(id)object forKey:(NSString *)key { if (!key.length) { - NSAssert(NO, @""); return; } if (object) { @@ -227,7 +357,6 @@ - (void)qmui_bindObject:(id)object forKey:(NSString *)key { - (void)qmui_bindObjectWeakly:(id)object forKey:(NSString *)key { if (!key.length) { - NSAssert(NO, @""); return; } if (object) { @@ -240,11 +369,10 @@ - (void)qmui_bindObjectWeakly:(id)object forKey:(NSString *)key { - (id)qmui_getBoundObjectForKey:(NSString *)key { if (!key.length) { - NSAssert(NO, @""); return nil; } id storedObj = [[self qmui_allBoundObjects] objectForKey:key]; - if ([storedObj isKindOfClass:[QMUIWeakObjectContainer class]]) { + if ([storedObj respondsToSelector:@selector(isQMUIWeakObjectContainer)] && ((QMUIWeakObjectContainer *)storedObj).isQMUIWeakObjectContainer) { storedObj = [(QMUIWeakObjectContainer *)storedObj object]; } return storedObj; @@ -313,3 +441,91 @@ - (BOOL)qmui_hasBindingKey:(NSString *)key { } @end + +@implementation NSObject (QMUI_Debug) + +BeginIgnorePerformSelectorLeaksWarning +- (NSString *)qmui_methodList { + return [self performSelector:NSSelectorFromString(@"_methodDescription")]; +} + +- (NSString *)qmui_shortMethodList { + return [self performSelector:NSSelectorFromString(@"_shortMethodDescription")]; +} + +- (NSString *)qmui_ivarList { + NSString *systemResult = [self performSelector:NSSelectorFromString(@"_ivarDescription")]; + NSRegularExpression *regx = [NSRegularExpression regularExpressionWithPattern:@"^(\\s+)(\\S+)" options:NSRegularExpressionCaseInsensitive error:nil]; + NSMutableArray *lines = [systemResult componentsSeparatedByString:@"\n"].mutableCopy; + [lines enumerateObjectsUsingBlock:^(NSString *line, NSUInteger idx, BOOL * _Nonnull stop) { + + // 过滤掉空行或者 struct 结尾的"}" + if (line.qmui_trim.length <= 2) return; + + // 有些 struct 类型的变量,会把 struct 的成员也缩进打出来,所以用这种方式过滤掉 + if ([line hasPrefix:@"\t\t"]) return; + + NSTextCheckingResult *regxResult = [regx firstMatchInString:line options:NSMatchingReportCompletion range:NSMakeRange(0, line.length)]; + if (regxResult.numberOfRanges < 3) return; + + NSRange indentRange = [regxResult rangeAtIndex:1]; + NSRange offsetRange = NSMakeRange(NSMaxRange(indentRange), 0); + NSRange ivarNameRange = [regxResult rangeAtIndex:2]; + NSString *ivarName = [line substringWithRange:ivarNameRange]; + Ivar ivar = class_getInstanceVariable(self.class, ivarName.UTF8String); + ptrdiff_t ivarOffset = ivar_getOffset(ivar); + NSString *lineWithOffset = [line stringByReplacingCharactersInRange:offsetRange withString:[NSString stringWithFormat:@"[%@|0x%@]", @(ivarOffset), [NSString stringWithFormat:@"%lx", (NSInteger)ivarOffset].uppercaseString]]; + [lines setObject:lineWithOffset atIndexedSubscript:idx]; + }]; + NSString *result = [lines componentsJoinedByString:@"\n"]; + return result; +} + +- (NSString *)qmui_viewInfo { + if ([self isKindOfClass:UIView.class]) { + return [self performSelector:NSSelectorFromString(@"recursiveDescription")]; + } + return @"仅支持 UIView"; +} +EndIgnorePerformSelectorLeaksWarning + +@end + +@implementation NSThread (QMUI_KVC) + +QMUISynthesizeBOOLProperty(qmui_shouldIgnoreUIKVCAccessProhibited, setQmui_shouldIgnoreUIKVCAccessProhibited) + +@end + +@interface NSException (QMUI_KVC) + +@end + +@implementation NSException (QMUI_KVC) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation(object_getClass([NSException class]), @selector(raise:format:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(NSObject *selfObject, NSExceptionName raise, NSString *format, ...) { + + if (raise == NSGenericException && [format isEqualToString:@"Access to %@'s %@ ivar is prohibited. This is an application bug"]) { + BOOL shouldIgnoreUIKVCAccessProhibited = ((QMUICMIActivated && IgnoreKVCAccessProhibited) || NSThread.currentThread.qmui_shouldIgnoreUIKVCAccessProhibited); + if (shouldIgnoreUIKVCAccessProhibited) return; + + QMUILogWarn(@"NSObject (QMUI)", @"使用 KVC 访问了 UIKit 的私有属性,会触发系统的 NSException,建议尽量避免此类操作,仍需访问可使用 BeginIgnoreUIKVCAccessProhibited 和 EndIgnoreUIKVCAccessProhibited 把相关代码包裹起来,或者直接使用 qmui_valueForKey: 、qmui_setValue:forKey:"); + } + + id (*originSelectorIMP)(id, SEL, NSExceptionName name, NSString *, ...); + originSelectorIMP = (id (*)(id, SEL, NSExceptionName name, NSString *, ...))originalIMPProvider(); + va_list args; + va_start(args, format); + NSString *reason = [[NSString alloc] initWithFormat:format arguments:args]; + originSelectorIMP(selfObject, originCMD, raise, reason); + va_end(args); + }; + }); + }); +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.h index e4425f18..829c5288 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSParagraphStyle+QMUI.h @@ -16,10 +16,11 @@ #import #import -@interface NSMutableParagraphStyle (QMUI) +@interface NSParagraphStyle (QMUI) /** - * 快速创建一个NSMutableParagraphStyle,等同于`qmui_paragraphStyleWithLineHeight:lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentLeft` + * 快速创建一个NSMutableParagraphStyle,等同于`qmui_paragraphStyleWithLineHeight:lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentLeft`。 + * 注意 NSParagraphStyle.lineBreakMode 默认值为 NSLineBreakByWordWrapping,而 UILabel.lineBreakMode 默认值为 NSLineBreakByTruncatingTail。如果 UILabel.attributedText 里显式设置了 NSParagraphStyle,则 UILabel.lineBreakMode 返回的值会由 attributedText 里的 NSParagraphStyle.lineBreakMode 决定。 * @param lineHeight 行高 * @return 一个NSMutableParagraphStyle对象 */ diff --git a/QMUI/QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.m index bf7fa6d7..bc1a8356 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSParagraphStyle+QMUI.m @@ -15,7 +15,7 @@ #import "NSParagraphStyle+QMUI.h" -@implementation NSMutableParagraphStyle (QMUI) +@implementation NSParagraphStyle (QMUI) + (instancetype)qmui_paragraphStyleWithLineHeight:(CGFloat)lineHeight { return [self qmui_paragraphStyleWithLineHeight:lineHeight lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentLeft]; @@ -26,7 +26,8 @@ + (instancetype)qmui_paragraphStyleWithLineHeight:(CGFloat)lineHeight lineBreakM } + (instancetype)qmui_paragraphStyleWithLineHeight:(CGFloat)lineHeight lineBreakMode:(NSLineBreakMode)lineBreakMode textAlignment:(NSTextAlignment)textAlignment { - NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + Class className = ![self isMemberOfClass:NSMutableParagraphStyle.class] ? NSMutableParagraphStyle.class : self;// 保证如果有 NSMutableParagraphStyle 的子类来调用这个方法,也可以用子类的 Class 去初始化 + NSMutableParagraphStyle *paragraphStyle = [[className alloc] init]; paragraphStyle.minimumLineHeight = lineHeight; paragraphStyle.maximumLineHeight = lineHeight; paragraphStyle.lineBreakMode = lineBreakMode; diff --git a/QMUI/QMUIKit/UIKitExtensions/NSPointerArray+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSPointerArray+QMUI.h index 4e63348a..d9c0f072 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSPointerArray+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSPointerArray+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSPointerArray+QMUI.h diff --git a/QMUI/QMUIKit/UIKitExtensions/NSPointerArray+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSPointerArray+QMUI.m index 4fde0151..e7cba777 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSPointerArray+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/NSPointerArray+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSPointerArray+QMUI.m @@ -15,7 +15,6 @@ #import "NSPointerArray+QMUI.h" #import "QMUICore.h" -#import @implementation NSPointerArray (QMUI) diff --git a/QMUI/QMUIKit/UIKitExtensions/NSRegularExpression+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSRegularExpression+QMUI.h new file mode 100644 index 00000000..d721a7bb --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSRegularExpression+QMUI.h @@ -0,0 +1,29 @@ +/** + * 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. + */ + +// +// NSRegularExpression+QMUI.h +// QMUIKit +// +// Created by QMUI Team on 2024/2/21. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSRegularExpression (QMUI) + +/// 某些场景频繁构造 NSRegularExpression 耗时较大,所以这里提供一个缓存的方式,如果你的场景非频繁,可以不用。 ++ (nullable NSRegularExpression *)qmui_cachedRegularExpressionWithPattern:(NSString *)pattern options:(NSRegularExpressionOptions)options; + +/// 某些场景频繁构造 NSRegularExpression 耗时较大,所以这里提供一个缓存的方式,如果你的场景非频繁,可以不用。等价于 options 为 NSRegularExpressionCaseInsensitive。 ++ (nullable NSRegularExpression *)qmui_cachedRegularExpressionWithPattern:(NSString *)pattern; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/NSRegularExpression+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSRegularExpression+QMUI.m new file mode 100644 index 00000000..a0138885 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSRegularExpression+QMUI.m @@ -0,0 +1,44 @@ +/** + * 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. + */ + +// +// NSRegularExpression+QMUI.m +// QMUIKit +// +// Created by QMUI Team on 2024/2/21. +// + +#import "NSRegularExpression+QMUI.h" + +@implementation NSRegularExpression (QMUI) + ++ (NSRegularExpression *)qmui_cachedRegularExpressionWithPattern:(NSString *)pattern options:(NSRegularExpressionOptions)options { + if (!pattern.length) return nil; + + static NSCache *cache = nil; + if (!cache) { + cache = [[NSCache alloc] init]; + cache.name = @"NSRegularExpression (QMUI)"; + cache.countLimit = 100; + } + + NSString *key = [NSString stringWithFormat:@"%@_%@", pattern, @(options)]; + NSRegularExpression *reg = [cache objectForKey:key]; + if (!reg) { + reg = [NSRegularExpression regularExpressionWithPattern:pattern options:options error:nil]; + if (!reg) return nil; + [cache setObject:reg forKey:key]; + } + return reg; +} + ++ (NSRegularExpression *)qmui_cachedRegularExpressionWithPattern:(NSString *)pattern { + return [self qmui_cachedRegularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive]; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/NSShadow+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSShadow+QMUI.h new file mode 100644 index 00000000..83bb2c7f --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSShadow+QMUI.h @@ -0,0 +1,26 @@ +/** + * 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. + */ +// +// NSShadow+QMUI.h +// QMUIKit +// +// Created by molice on 2022/9/6. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSShadow (QMUI) + ++ (instancetype)qmui_shadowWithColor:(nullable UIColor *)shadowColor + shadowOffset:(CGSize)shadowOffset + shadowRadius:(CGFloat)shadowRadius; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuBaseItem.h b/QMUI/QMUIKit/UIKitExtensions/NSShadow+QMUI.m similarity index 57% rename from QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuBaseItem.h rename to QMUI/QMUIKit/UIKitExtensions/NSShadow+QMUI.m index 7ae27d1c..7374dc78 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuBaseItem.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSShadow+QMUI.m @@ -1,24 +1,27 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ - + */ // -// QMUIPopupMenuBaseItem.h +// NSShadow+QMUI.m // QMUIKit // -// Created by QMUI Team on 2018/8/21. +// Created by molice on 2022/9/6. // -#import -#import "QMUIPopupMenuItemProtocol.h" +#import "NSShadow+QMUI.h" -/** - 用于 QMUIPopupMenuView 的 item 基类,便于自定义各种类型的 item。若有 subview 请直接添加到 self 上,自身大小的计算请写到 sizeThatFits:,布局写到 layoutSubviews。 - */ -@interface QMUIPopupMenuBaseItem : UIView +@implementation NSShadow (QMUI) + ++ (instancetype)qmui_shadowWithColor:(UIColor *)shadowColor shadowOffset:(CGSize)shadowOffset shadowRadius:(CGFloat)shadowRadius { + NSShadow *shadow = NSShadow.new; + shadow.shadowColor = shadowColor; + shadow.shadowOffset = shadowOffset; + shadow.shadowBlurRadius = shadowRadius; + return shadow; +} @end diff --git a/QMUI/QMUIKit/UIKitExtensions/NSString+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSString+QMUI.h index 987c4d3a..38a3facd 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSString+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSString+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSString+QMUI.h @@ -14,37 +14,11 @@ // #import -#import - -@interface NSString (QMUI) - -/// 将字符串按一个一个字符拆成数组,类似 JavaScript 里的 split(""),如果多个空格,则每个空格也会当成一个 item -@property(readonly, copy) NSArray *qmui_toArray; - -/// 将字符串按一个一个字符拆成数组,类似 JavaScript 里的 split(""),但会自动过滤掉空白字符 -@property(readonly, copy) NSArray *qmui_toTrimmedArray; - -/// 去掉头尾的空白字符 -@property(readonly, copy) NSString *qmui_trim; +#import -/// 去掉整段文字内的所有空白字符(包括换行符) -@property(readonly, copy) NSString *qmui_trimAllWhiteSpace; +NS_ASSUME_NONNULL_BEGIN -/// 将文字中的换行符替换为空格 -@property(readonly, copy) NSString *qmui_trimLineBreakCharacter; - -/// 把该字符串转换为对应的 md5 -@property(readonly, copy) NSString *qmui_md5; - -/// 返回一个符合 query value 要求的编码后的字符串,例如&、#、=等字符均会被变为 %xxx 的编码 -/// @see `NSCharacterSet (QMUI) qmui_URLUserInputQueryAllowedCharacterSet` -@property(readonly, copy) NSString *qmui_stringByEncodingUserInputQuery; - -/** - * 用正则表达式匹配的方式去除字符串里一些特殊字符,避免UI上的展示问题 - * @link http://www.croton.su/en/uniblock/Diacriticals.html @/link - */ -@property(readonly, copy) NSString *qmui_removeMagicalChar; +@protocol QMUIStringProtocol /** * 按照中文 2 个字符、英文 1 个字符的方式来计算文本长度 @@ -57,18 +31,18 @@ * 例如对于字符串“😊😞”,它的长度为4,若调用 [string qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:1],将返回“😊😞”。 * 若调用系统的 [string substringFromIndex:1],将返回“?😞”。(?表示乱码,因为第一个 emoji 表情被从中间裁开了)。 * - * @param index 要从哪个 index 开始裁剪文字 + * @param index 要从哪个 index 开始裁剪文字,如果 countingNonASCIICharacterAsTwo 为 YES,则 index 也要按 YES 的方式来算 * @param lessValue 要按小的长度取,还是按大的长度取 * @param countingNonASCIICharacterAsTwo 是否按照 英文 1 个字符长度、中文 2 个字符长度的方式来裁剪 * @return 裁剪完的字符 */ -- (NSString *)qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo; +- (nullable instancetype)qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo; /** * 相当于 `qmui_substringAvoidBreakingUpCharacterSequencesFromIndex: lessValue:YES` countingNonASCIICharacterAsTwo:NO * @see qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:lessValue:countingNonASCIICharacterAsTwo: */ -- (NSString *)qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index; +- (nullable instancetype)qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index; /** * 将字符串从开头裁剪到指定的 index,裁剪时会避免将 emoji 等 "character sequences" 拆散(一个 emoji 表情占用1-4个长度的字符)。 @@ -76,18 +50,18 @@ * 例如对于字符串“😊😞”,它的长度为4,若调用 [string qmui_substringAvoidBreakingUpCharacterSequencesToIndex:1 lessValue:NO countingNonASCIICharacterAsTwo:NO],将返回“😊”。 * 若调用系统的 [string substringToIndex:1],将返回“?”。(?表示乱码,因为第一个 emoji 表情被从中间裁开了)。 * - * @param index 要裁剪到哪个 index + * @param index 要裁剪到哪个 index 为止(不包含该 index,策略与系统的 substringToIndex: 一致),如果 countingNonASCIICharacterAsTwo 为 YES,则 index 也要按 YES 的方式来算 * @param lessValue 裁剪时若遇到“character sequences”,是向下取整还是向上取整。 * @param countingNonASCIICharacterAsTwo 是否按照 英文 1 个字符长度、中文 2 个字符长度的方式来裁剪 * @return 裁剪完的字符 */ -- (NSString *)qmui_substringAvoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo; +- (nullable instancetype)qmui_substringAvoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo; /** * 相当于 `qmui_substringAvoidBreakingUpCharacterSequencesToIndex:lessValue:YES` countingNonASCIICharacterAsTwo:NO * @see qmui_substringAvoidBreakingUpCharacterSequencesToIndex:lessValue:countingNonASCIICharacterAsTwo: */ -- (NSString *)qmui_substringAvoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index; +- (nullable instancetype)qmui_substringAvoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index; /** * 将字符串里指定 range 的子字符串裁剪出来,会避免将 emoji 等 "character sequences" 拆散(一个 emoji 表情占用1-4个长度的字符)。 @@ -96,29 +70,64 @@ * 在非 lessValue 模式下,裁剪 (0, 1) 或 (0, 2),得到的都是“😊”。 * * @param range 要裁剪的文字位置 - * @param lessValue 裁剪时若遇到“character sequences”,是向下取整还是向上取整。 + * @param lessValue 裁剪时若遇到“character sequences”,是向下取整还是向上取整(系统的 rangeOfComposedCharacterSequencesForRange: 会尽量把给定 range 里包含的所有 character sequences 都包含在内,也即 lessValue = NO)。 * @param countingNonASCIICharacterAsTwo 是否按照 英文 1 个字符长度、中文 2 个字符长度的方式来裁剪 * @return 裁剪完的字符 */ -- (NSString *)qmui_substringAvoidBreakingUpCharacterSequencesWithRange:(NSRange)range lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo; +- (nullable instancetype)qmui_substringAvoidBreakingUpCharacterSequencesWithRange:(NSRange)range lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo; /** * 相当于 `qmui_substringAvoidBreakingUpCharacterSequencesWithRange:lessValue:YES` countingNonASCIICharacterAsTwo:NO * @see qmui_substringAvoidBreakingUpCharacterSequencesWithRange:lessValue:countingNonASCIICharacterAsTwo: */ -- (NSString *)qmui_substringAvoidBreakingUpCharacterSequencesWithRange:(NSRange)range; +- (nullable instancetype)qmui_substringAvoidBreakingUpCharacterSequencesWithRange:(NSRange)range; /** * 移除指定位置的字符,可兼容emoji表情的情况(一个emoji表情占1-4个length) * @param index 要删除的位置 */ -- (NSString *)qmui_stringByRemoveCharacterAtIndex:(NSUInteger)index; +- (nullable instancetype)qmui_stringByRemoveCharacterAtIndex:(NSUInteger)index; /** * 移除最后一个字符,可兼容emoji表情的情况(一个emoji表情占1-4个length) * @see `qmui_stringByRemoveCharacterAtIndex:` */ -- (NSString *)qmui_stringByRemoveLastCharacter; +- (nullable instancetype)qmui_stringByRemoveLastCharacter; + +@end + +@interface NSString (QMUI) + +/// 将字符串按一个一个字符拆成数组,类似 JavaScript 里的 split(""),如果多个空格,则每个空格也会当成一个 item +@property(nullable, readonly, copy) NSArray *qmui_toArray; + +/// 将字符串按一个一个字符拆成数组,类似 JavaScript 里的 split(""),但会自动过滤掉空白字符 +@property(nullable, readonly, copy) NSArray *qmui_toTrimmedArray; + +/// 去掉头尾的空白字符 +@property(readonly, copy) NSString *qmui_trim; + +/// 去掉整段文字内的所有空白字符(包括换行符) +@property(readonly, copy) NSString *qmui_trimAllWhiteSpace; + +/// 将文字中的换行符替换为空格 +@property(readonly, copy) NSString *qmui_trimLineBreakCharacter; + +/// 把该字符串转换为对应的 md5 +@property(readonly, copy) NSString *qmui_md5; + +/// 返回一个符合 query value 要求的编码后的字符串,例如&、#、=等字符均会被变为 %xxx 的编码 +/// @see `NSCharacterSet (QMUI) qmui_URLUserInputQueryAllowedCharacterSet` +@property(nullable, readonly, copy) NSString *qmui_stringByEncodingUserInputQuery; + +/// 把当前文本的第一个字符改为大写,其他的字符保持不变,例如 backgroundView.qmui_capitalizedString -> BackgroundView(系统的 capitalizedString 会变成 Backgroundview) +@property(nullable, readonly, copy) NSString *qmui_capitalizedString; + +/** + * 用正则表达式匹配的方式去除字符串里一些特殊字符,避免UI上的展示问题 + * @link http://www.croton.su/en/uniblock/Diacriticals.html @/link + */ +@property(nullable, readonly, copy) NSString *qmui_removeMagicalChar; /** 用正则表达式匹配字符串,将匹配到的第一个结果返回,大小写不敏感 @@ -126,7 +135,25 @@ @param pattern 正则表达式 @return 匹配到的第一个结果,如果没有匹配成功则返回 nil */ -- (NSString *)qmui_stringMatchedByPattern:(NSString *)pattern; +- (nullable NSString *)qmui_stringMatchedByPattern:(NSString *)pattern; + +/** + 用正则表达式匹配字符串,返回匹配到的第一个结果里的指定分组(由参数 index 指定)。 + 例如使用 @"ing([\\d\\.]+)" 表达式匹配字符串 @"string0.05" 并指定参数 index = 1,则返回 @"0.05"。 + @param pattern 正则表达式,可用括号表示分组 + @param index 要返回第几个分组,0表示整个正则表达式匹配到的结果,1表示匹配到的结果里的第1个分组(第1个括号) + @return 返回匹配到的第一个结果里的指定分组,如果 index 超过总分组数则返回 nil。匹配失败也返回 nil。 + */ +- (nullable NSString *)qmui_stringMatchedByPattern:(NSString *)pattern groupIndex:(NSInteger)index; + +/** + 用正则表达式匹配字符串,返回匹配到的第一个结果里的指定分组(由参数 name 指定)。 + 例如使用 @"ing(?[\\d\\.]+)" 表达式匹配字符串 @"string0.05" 并指定参数 name 为 @"number",则返回 @"0.05"。 + @param pattern 正则表达式,可用括号表示分组,分组必须用 ? 的语法来为分组命名。 + @param name 要返回的分组名称,可通过 pattern 里的 ? 语法对分组进行命名。 + @return 返回匹配到的第一个结果里的指定分组,如果 name 不存在则返回 nil。匹配失败也返回 nil。 + */ +- (nullable NSString *)qmui_stringMatchedByPattern:(NSString *)pattern groupName:(NSString *)name; /** * 用正则表达式匹配字符串并将其替换为指定的另一个字符串,大小写不敏感 @@ -151,7 +178,9 @@ @interface NSString (QMUI_StringFormat) -+ (instancetype)qmui_stringWithNSInteger:(NSInteger)integerValue; -+ (instancetype)qmui_stringWithCGFloat:(CGFloat)floatValue; -+ (instancetype)qmui_stringWithCGFloat:(CGFloat)floatValue decimal:(NSUInteger)decimal; ++ (NSString *)qmui_stringWithNSInteger:(NSInteger)integerValue; ++ (NSString *)qmui_stringWithCGFloat:(CGFloat)floatValue; ++ (NSString *)qmui_stringWithCGFloat:(CGFloat)floatValue decimal:(NSUInteger)decimal; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/NSString+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSString+QMUI.m index ee0d9cf0..b2fa90ad 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSString+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/NSString+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSString+QMUI.m @@ -15,9 +15,11 @@ #import "NSString+QMUI.h" #import +#import "QMUICore.h" #import "NSArray+QMUI.h" #import "NSCharacterSet+QMUI.h" -#import +#import "QMUIStringPrivate.h" +#import "NSRegularExpression+QMUI.h" @implementation NSString (QMUI) @@ -41,7 +43,9 @@ @implementation NSString (QMUI) } - (NSString *)qmui_trim { - return [self stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSMutableCharacterSet * characterSet = [NSMutableCharacterSet whitespaceAndNewlineCharacterSet]; + [characterSet addCharactersInString:@"\0"]; + return [self stringByTrimmingCharactersInSet:characterSet]; } - (NSString *)qmui_trimAllWhiteSpace { @@ -55,7 +59,9 @@ - (NSString *)qmui_trimLineBreakCharacter { - (NSString *)qmui_md5 { const char *cStr = [self UTF8String]; unsigned char result[CC_MD5_DIGEST_LENGTH]; + BeginIgnoreDeprecatedWarning CC_MD5(cStr, (CC_LONG)strlen(cStr), result); + EndIgnoreDeprecatedWarning return [NSString stringWithFormat: @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", result[0], result[1], result[2], result[3], @@ -68,8 +74,19 @@ - (NSString *)qmui_stringByEncodingUserInputQuery { return [self stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet qmui_URLUserInputQueryAllowedCharacterSet]]; } +- (NSString *)qmui_capitalizedString { + if (self.length) { + NSRange range = [self rangeOfComposedCharacterSequenceAtIndex:0]; + if (range.length > 1) { + return self;// 说明这个字符没法大写 + } + return [NSString stringWithFormat:@"%@%@", [self substringToIndex:1].uppercaseString, [self substringFromIndex:1]].copy; + } + return nil; +} + + (NSString *)hexLetterStringWithInteger:(NSInteger)integer { - NSAssert(integer < 16, @"要转换的数必须是16进制里的个位数,也即小于16,但你传给我是%@", @(integer)); + QMUIAssert(integer < 16, @"NSString (QMUI)", @"%s 参数仅接受小于16的值,当前传入的是 %@", __func__, @(integer)); NSString *letter = nil; switch (integer) { @@ -142,148 +159,111 @@ - (NSString *)qmui_removeMagicalChar { return self; } - NSError *error = nil; - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[\u0300-\u036F]" options:NSRegularExpressionCaseInsensitive error:&error]; + NSRegularExpression *regex = [NSRegularExpression qmui_cachedRegularExpressionWithPattern:@"[\u0300-\u036F]"]; NSString *modifiedString = [regex stringByReplacingMatchesInString:self options:NSMatchingReportProgress range:NSMakeRange(0, self.length) withTemplate:@""]; return modifiedString; } -- (NSUInteger)qmui_lengthWhenCountingNonASCIICharacterAsTwo { - NSUInteger length = 0; - for (NSUInteger i = 0, l = self.length; i < l; i++) { - unichar character = [self characterAtIndex:i]; - if (isascii(character)) { - length += 1; - } else { - length += 2; - } +- (NSString *)qmui_stringMatchedByPattern:(NSString *)pattern { + return [self qmui_stringMatchedByPattern:pattern groupIndex:0]; +} + +- (NSString *)qmui_stringMatchedByPattern:(NSString *)pattern groupIndex:(NSInteger)index { + if (pattern.length <= 0 || index < 0) return nil; + + NSRegularExpression *regx = [NSRegularExpression qmui_cachedRegularExpressionWithPattern:pattern]; + NSTextCheckingResult *result = [regx firstMatchInString:self options:NSMatchingReportCompletion range:NSMakeRange(0, self.length)]; + if (result.numberOfRanges > index) { + NSRange range = [result rangeAtIndex:index]; + return [self substringWithRange:range]; } - return length; + return nil; } -- (NSUInteger)transformIndexToDefaultModeWithIndex:(NSUInteger)index { - CGFloat strlength = 0.f; - NSUInteger i = 0; - for (i = 0; i < self.length; i++) { - unichar character = [self characterAtIndex:i]; - if (isascii(character)) { - strlength += 1; - } else { - strlength += 2; +- (NSString *)qmui_stringMatchedByPattern:(NSString *)pattern groupName:(NSString *)name { + if (pattern.length <= 0) return nil; + + NSRegularExpression *regx = [NSRegularExpression qmui_cachedRegularExpressionWithPattern:pattern]; + NSTextCheckingResult *result = [regx firstMatchInString:self options:NSMatchingReportCompletion range:NSMakeRange(0, self.length)]; + if (result.numberOfRanges > 1) { + NSRange range = [result rangeWithName:name]; + QMUIAssert(range.location != NSNotFound, @"NSString (QMUI)", @"%s, 不存在名为 %@ 的 group name", __func__, name); + if (range.location != NSNotFound) { + return [self substringWithRange:range]; } - if (strlength >= index + 1) return i; } - return 0; + + return nil; +} + +- (NSString *)qmui_stringByReplacingPattern:(NSString *)pattern withString:(NSString *)replacement { + NSRegularExpression *regex = [NSRegularExpression qmui_cachedRegularExpressionWithPattern:pattern]; + if (!regex) { + return self; + } + return [regex stringByReplacingMatchesInString:self options:NSMatchingReportCompletion range:NSMakeRange(0, self.length) withTemplate:replacement]; } -- (NSRange)transformRangeToDefaultModeWithRange:(NSRange)range { - CGFloat strlength = 0.f; - NSRange resultRange = NSMakeRange(NSNotFound, 0); - NSUInteger i = 0; - for (i = 0; i < self.length; i++) { +#pragma mark - + +- (NSUInteger)qmui_lengthWhenCountingNonASCIICharacterAsTwo { + NSUInteger length = 0; + for (NSUInteger i = 0, l = self.length; i < l; i++) { unichar character = [self characterAtIndex:i]; if (isascii(character)) { - strlength += 1; + length += 1; } else { - strlength += 2; - } - if (strlength >= range.location + 1) { - if (resultRange.location == NSNotFound) { - resultRange.location = i; - } - - if (range.length > 0 && strlength >= NSMaxRange(range)) { - resultRange.length = i - resultRange.location + (strlength == NSMaxRange(range) ? 1 : 0); - return resultRange; - } + length += 2; } } - return resultRange; + return length; } -- (NSString *)qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { - index = countingNonASCIICharacterAsTwo ? [self transformIndexToDefaultModeWithIndex:index] : index; - NSRange range = [self rangeOfComposedCharacterSequenceAtIndex:index]; - return [self substringFromIndex:lessValue ? NSMaxRange(range) : range.location]; +- (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { + return [QMUIStringPrivate substring:self avoidBreakingUpCharacterSequencesFromIndex:index lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; } -- (NSString *)qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index { +- (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index { return [self qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:index lessValue:YES countingNonASCIICharacterAsTwo:NO]; } -- (NSString *)qmui_substringAvoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { - index = countingNonASCIICharacterAsTwo ? [self transformIndexToDefaultModeWithIndex:index] : index; - NSRange range = [self rangeOfComposedCharacterSequenceAtIndex:index]; - return [self substringToIndex:lessValue ? range.location : NSMaxRange(range)]; +- (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { + return [QMUIStringPrivate substring:self avoidBreakingUpCharacterSequencesToIndex:index lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; } -- (NSString *)qmui_substringAvoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index { +- (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index { return [self qmui_substringAvoidBreakingUpCharacterSequencesToIndex:index lessValue:YES countingNonASCIICharacterAsTwo:NO]; } -- (NSString *)qmui_substringAvoidBreakingUpCharacterSequencesWithRange:(NSRange)range lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { - range = countingNonASCIICharacterAsTwo ? [self transformRangeToDefaultModeWithRange:range] : range; - NSRange characterSequencesRange = lessValue ? [self downRoundRangeOfComposedCharacterSequencesForRange:range] : [self rangeOfComposedCharacterSequencesForRange:range]; - NSString *resultString = [self substringWithRange:characterSequencesRange]; - return resultString; +- (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesWithRange:(NSRange)range lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { + return [QMUIStringPrivate substring:self avoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; } -- (NSString *)qmui_substringAvoidBreakingUpCharacterSequencesWithRange:(NSRange)range { +- (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesWithRange:(NSRange)range { return [self qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:YES countingNonASCIICharacterAsTwo:NO]; } -- (NSRange)downRoundRangeOfComposedCharacterSequencesForRange:(NSRange)range { - if (range.length == 0) { - return range; - } - - NSRange resultRange = [self rangeOfComposedCharacterSequencesForRange:range]; - if (NSMaxRange(resultRange) > NSMaxRange(range)) { - return [self downRoundRangeOfComposedCharacterSequencesForRange:NSMakeRange(range.location, range.length - 1)]; - } - return resultRange; +- (instancetype)qmui_stringByRemoveCharacterAtIndex:(NSUInteger)index { + return [QMUIStringPrivate string:self avoidBreakingUpCharacterSequencesByRemoveCharacterAtIndex:index]; } -- (NSString *)qmui_stringByRemoveCharacterAtIndex:(NSUInteger)index { - NSRange rangeForRemove = [self rangeOfComposedCharacterSequenceAtIndex:index]; - NSString *resultString = [self stringByReplacingCharactersInRange:rangeForRemove withString:@""]; - return resultString; -} - -- (NSString *)qmui_stringByRemoveLastCharacter { +- (instancetype)qmui_stringByRemoveLastCharacter { return [self qmui_stringByRemoveCharacterAtIndex:self.length - 1]; } -- (NSString *)qmui_stringMatchedByPattern:(NSString *)pattern { - NSRange range = [self rangeOfString:pattern options:NSRegularExpressionSearch|NSCaseInsensitiveSearch]; - if (range.location != NSNotFound) { - return [self substringWithRange:range]; - } - return nil; -} - -- (NSString *)qmui_stringByReplacingPattern:(NSString *)pattern withString:(NSString *)replacement { - NSError *error = nil; - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error]; - if (error) { - return self; - } - return [regex stringByReplacingMatchesInString:self options:NSMatchingReportCompletion range:NSMakeRange(0, self.length) withTemplate:replacement]; -} - @end @implementation NSString (QMUI_StringFormat) -+ (instancetype)qmui_stringWithNSInteger:(NSInteger)integerValue { - return [NSString stringWithFormat:@"%@", @(integerValue)]; ++ (NSString *)qmui_stringWithNSInteger:(NSInteger)integerValue { + return @(integerValue).stringValue; } -+ (instancetype)qmui_stringWithCGFloat:(CGFloat)floatValue { ++ (NSString *)qmui_stringWithCGFloat:(CGFloat)floatValue { return [NSString qmui_stringWithCGFloat:floatValue decimal:2]; } -+ (instancetype)qmui_stringWithCGFloat:(CGFloat)floatValue decimal:(NSUInteger)decimal { ++ (NSString *)qmui_stringWithCGFloat:(CGFloat)floatValue decimal:(NSUInteger)decimal { NSString *formatString = [NSString stringWithFormat:@"%%.%@f", @(decimal)]; return [NSString stringWithFormat:formatString, floatValue]; } diff --git a/QMUI/QMUIKit/UIKitExtensions/NSURL+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSURL+QMUI.h index f227a6b2..a6e6c43a 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSURL+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSURL+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSURL+QMUI.h diff --git a/QMUI/QMUIKit/UIKitExtensions/NSURL+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSURL+QMUI.m index 95fee833..811d6fd2 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSURL+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/NSURL+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // NSURL+QMUI.m @@ -27,7 +27,7 @@ @implementation NSURL (QMUI) [urlComponents.queryItems enumerateObjectsUsingBlock:^(NSURLQueryItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if (obj.name) { - [params setObject:obj.value ?: [NSNull null] forKey:obj.name]; + [params setObject:obj.value ?: @"" forKey:obj.name]; } }]; return [params copy]; diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocol.h b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocol.h new file mode 100644 index 00000000..2c31ef77 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocol.h @@ -0,0 +1,61 @@ +/** + * 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. + */ +// +// QMUIBarProtocol.h +// QMUIKit +// +// Created by molice on 2022/5/18. +// Copyright © 2022 QMUI Team. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + UINavigationBar、UITabBar 在一些特性上基本相同,但它们又是分别继承自 UIView 的,导致很多属性、方法都需要两边添加,所以这里建了个协议,分别在 UINavigationBar、UITabBar 里实现,以保证两边的功能是相同的。 + */ +@protocol QMUIBarProtocol + +/** + bar 的背景 view,可能显示磨砂、背景图。 + 在 iOS 10 及以后是私有的 _UIBarBackground 类。 + 在 iOS 9 及以前是私有的类,对 UINavigationBar 来说是 _UINavigationBarBackground,对 UITabBar 来说是 _UITabBarBackgroundView。 + */ +@property(nullable, nonatomic, strong, readonly) UIView *qmui_backgroundView; + +/** + qmui_backgroundView 内的 subview,用于显示分隔线 shadowImage,注意这个 view 是溢出到 qmui_backgroundView 外的。若 shadowImage 为 [UIImage new],则这个 view 的高度为 0。 + */ +@property(nullable, nonatomic, strong, readonly) UIImageView *qmui_shadowImageView; + +/** + 获取 bar 里面的磨砂背景,具体的 view 层级是 UIBar → _UIBarBackground → UIVisualEffectView。仅在 bar 的样式确定之后系统才会创建。 + iOS 15 及以后,bar 里可能会同时存在多个磨砂背景(详见 @c qmui_effectViews ),这个属性会获取其中正在显示的那个磨砂,如果两个都在显示,则取 view 层级树里更上层的那个。 + */ +@property(nullable, nonatomic, strong, readonly) UIVisualEffectView *qmui_effectView; + +/** + iOS 15 及以后,由于 bar 的样式在滚动到顶部和底部会有不同,所以可能同时存在两个 effectView。 + */ +@property(nullable, nonatomic, strong, readonly) NSArray *qmui_effectViews; + +/** + 允许直接指定 tab 具体的磨砂样式(系统的仅在 iOS 13 及以后用 UINavigation(Tab)BarAppearance.backgroundEffects 才可以实现)。默认为 nil,如果你没设置过这个属性,那么 nil 的行为就是维持系统的样式,但如果你主动设置过这个属性,那么后续的 nil 则表示把磨砂清空(也即可能出现背景透明的 bar)。 + @note 生效的前提是 backgroundImage、barTintColor 都为空,因为这两者的优先级都比磨砂高。 + */ +@property(nullable, nonatomic, strong) UIBlurEffect *qmui_effect; + +/** + 当 tabBar 展示磨砂的样式时,可以通过这个属性精准指定磨砂的前景色(可参考 CALayer(QMUI).qmui_foregroundColor),因为系统的某些 UIBlurEffectStyle 会自带前景色,且不可去掉,那种情况下你就无法得到准确的自定义前景色了(即便你试图通过设置半透明的 barTintColor 来达到前景色的效果,那也依然会叠加一层系统自带的半透明前景色)。 + */ +@property(nullable, nonatomic, strong) UIColor *qmui_effectForegroundColor; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.h b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.h new file mode 100644 index 00000000..0798a280 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.h @@ -0,0 +1,34 @@ +/** + * 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. + */ +// +// QMUIBarProtocolPrivate.h +// QMUIKit +// +// Created by molice on 2022/5/18. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol QMUIBarProtocolPrivate + +@required +@property(nonatomic, assign) BOOL qmuibar_hasSetEffect; +@property(nonatomic, assign) BOOL qmuibar_hasSetEffectForegroundColor; +@property(nonatomic, strong, readonly, nullable) NSArray *qmuibar_backgroundEffects; +- (void)qmuibar_updateEffect; +@end + +@interface QMUIBarProtocolPrivate : NSObject + ++ (void)swizzleBarBackgroundViewIfNeeded; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.m b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.m new file mode 100644 index 00000000..9ea771c9 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.m @@ -0,0 +1,84 @@ +/** + * 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. + */ +// +// QMUIBarProtocolPrivate.m +// QMUIKit +// +// Created by molice on 2022/5/18. +// + +#import "QMUIBarProtocolPrivate.h" +#import "QMUIBarProtocol.h" +#import "QMUICore.h" + +@implementation QMUIBarProtocolPrivate + ++ (void)swizzleBarBackgroundViewIfNeeded { + [QMUIHelper executeBlock:^{ + Class backgroundClass = NSClassFromString(@"_UIBarBackground"); + + OverrideImplementation(backgroundClass, @selector(didMoveToSuperview), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject) { + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + if ([selfObject.superview conformsToProtocol:@protocol(QMUIBarProtocol)]) { + id bar = (id)selfObject.superview; + if (bar.qmuibar_hasSetEffect || bar.qmuibar_hasSetEffectForegroundColor) { + [bar qmuibar_updateEffect]; + } + } + }; + }); + + OverrideImplementation(backgroundClass, @selector(didAddSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, UIView *subview) { + + // call super + void (*originSelectorIMP)(id, SEL, UIView *); + originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, subview); + + // 注意可能存在多个 UIVisualEffectView,例如用于 shadowImage 的 _UIBarBackgroundShadowView,需要过滤掉 + if ([subview isMemberOfClass:UIVisualEffectView.class] + && [selfObject.superview conformsToProtocol:@protocol(QMUIBarProtocol)]) { + id bar = (id)selfObject.superview; + if (bar.qmuibar_hasSetEffect || bar.qmuibar_hasSetEffectForegroundColor) { + [bar qmuibar_updateEffect]; + } + } + }; + }); + + // 系统会在任意可能的时机去刷新 backgroundEffects,为了避免被系统的值覆盖,这里需要重写它 + OverrideImplementation(UIVisualEffectView.class, NSSelectorFromString(@"setBackgroundEffects:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIVisualEffectView *selfObject, NSArray *firstArgv) { + + if ([selfObject.superview isKindOfClass:backgroundClass] + && [selfObject.superview.superview conformsToProtocol:@protocol(QMUIBarProtocol)]) { + id bar = (id)selfObject.superview.superview; + if (bar.qmui_effectView == selfObject) { + if (bar.qmuibar_hasSetEffect) { + firstArgv = bar.qmuibar_backgroundEffects; + } + } + } + + // call super + void (*originSelectorIMP)(id, SEL, NSArray *); + originSelectorIMP = (void (*)(id, SEL, NSArray *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + }; + }); + } oncePerIdentifier:@"QMUIBarProtocolPrivate"]; +} + +@end diff --git a/QMUI/qmui/main.m b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.h similarity index 66% rename from QMUI/qmui/main.m rename to QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.h index bb5528cc..612932a9 100644 --- a/QMUI/qmui/main.m +++ b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.h @@ -1,24 +1,23 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ - + */ // -// main.m -// qmui +// UINavigationBar+QMUIBarProtocol.h +// QMUIKit // -// Created by QMUI Team on 14-6-22. +// Created by molice on 2022/5/18. // #import -#import "QMUIAppDelegate.h" +#import "QMUIBarProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface UINavigationBar (QMUIBarProtocol) +@end -int main(int argc, char * argv[]) -{ - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([QMUIAppDelegate class])); - } -} +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.m b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.m new file mode 100644 index 00000000..a5948dc1 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.m @@ -0,0 +1,119 @@ +/** + * 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. + */ +// +// UINavigationBar+QMUIBarProtocol.m +// QMUIKit +// +// Created by molice on 2022/5/18. +// + +#import "UINavigationBar+QMUIBarProtocol.h" +#import "QMUIBarProtocolPrivate.h" +#import "QMUICore.h" +#import "UIVisualEffectView+QMUI.h" +#import "NSArray+QMUI.h" + +@interface UINavigationBar () +@end + +@implementation UINavigationBar (QMUIBarProtocol) + +QMUISynthesizeBOOLProperty(qmuibar_hasSetEffect, setQmuibar_hasSetEffect) +QMUISynthesizeBOOLProperty(qmuibar_hasSetEffectForegroundColor, setQmuibar_hasSetEffectForegroundColor) + +BeginIgnoreClangWarning(-Wobjc-protocol-method-implementation) +- (void)qmuibar_updateEffect { + [self.qmui_effectViews enumerateObjectsUsingBlock:^(UIVisualEffectView * _Nonnull effectView, NSUInteger idx, BOOL * _Nonnull stop) { + if (self.qmuibar_hasSetEffect) { + // 这里对 iOS 13 不使用 UITabBarAppearance.backgroundEffect 来修改,是因为反正不管 iOS 10 还是 13,最终都是 setBackgroundEffects: 在起作用,而且不用 UITabBarAppearance 还可以规避与 UIAppearance 机制的冲突 + NSArray *effects = self.qmuibar_backgroundEffects; + [effectView qmui_performSelector:NSSelectorFromString(@"setBackgroundEffects:") withArguments:&effects, nil]; + } + if (self.qmuibar_hasSetEffectForegroundColor) { + effectView.qmui_foregroundColor = self.qmui_effectForegroundColor; + } + }]; +} +EndIgnoreClangWarning + +// UITabBar、UIVisualEffectView 都有一个私有的方法 backgroundEffects,当 UIVisualEffectView 应用于 UITabBar 场景时,磨砂的效果实际上被放在 backgroundEffects 内,而不是公开接口的 effect 属性里,这里为了方便,将 UITabBar (QMUI).effect 转成可用于 backgroundEffects 的数组 +- (NSArray *)qmuibar_backgroundEffects { + if (self.qmuibar_hasSetEffect) { + return self.qmui_effect ? @[self.qmui_effect] : nil; + } + return nil; +} + +#pragma mark - + +- (UIView *)qmui_backgroundView { + return [self qmui_valueForKey:@"_backgroundView"]; +} + +- (UIImageView *)qmui_shadowImageView { + // bar 在 init 完就可以获取到 backgroundView 和 shadowView,无需关心调用时机的问题 + return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView1"]; +} + +- (UIVisualEffectView *)qmui_effectView { + NSArray *visibleEffectViews = [self.qmui_effectViews qmui_filterWithBlock:^BOOL(UIVisualEffectView * _Nonnull item) { + return !item.hidden && item.alpha > 0.01 && item.superview; + }]; + return visibleEffectViews.lastObject; +} + +- (NSArray *)qmui_effectViews { + UIView *backgroundView = self.qmui_backgroundView; + NSMutableArray *result = NSMutableArray.new; + UIVisualEffectView *backgroundEffectView1 = [backgroundView valueForKey:@"_effectView1"]; + UIVisualEffectView *backgroundEffectView2 = [backgroundView valueForKey:@"_effectView2"]; + if (backgroundEffectView1) { + [result addObject:backgroundEffectView1]; + } + if (backgroundEffectView2) { + [result addObject:backgroundEffectView2]; + } + return result.count > 0 ? result : nil; +} + +static char kAssociatedObjectKey_effect; +- (void)setQmui_effect:(UIBlurEffect *)qmui_effect { + if (qmui_effect) { + [QMUIBarProtocolPrivate swizzleBarBackgroundViewIfNeeded]; + } + + BOOL valueChanged = self.qmui_effect != qmui_effect; + objc_setAssociatedObject(self, &kAssociatedObjectKey_effect, qmui_effect, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (valueChanged) { + self.qmuibar_hasSetEffect = YES;// QMUITheme 切换时会重新赋值,所以可能出现本来就是 nil,还给它又赋值了 nil,这种场景不应该导致 hasSet 标志位改变,所以要把标志位的设置放在 if (valueChanged) 里 + [self qmuibar_updateEffect]; + } +} + +- (UIBlurEffect *)qmui_effect { + return (UIBlurEffect *)objc_getAssociatedObject(self, &kAssociatedObjectKey_effect); +} + +static char kAssociatedObjectKey_effectForegroundColor; +- (void)setQmui_effectForegroundColor:(UIColor *)qmui_effectForegroundColor { + if (qmui_effectForegroundColor) { + [QMUIBarProtocolPrivate swizzleBarBackgroundViewIfNeeded]; + } + BOOL valueChanged = ![self.qmui_effectForegroundColor isEqual:qmui_effectForegroundColor]; + objc_setAssociatedObject(self, &kAssociatedObjectKey_effectForegroundColor, qmui_effectForegroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (valueChanged) { + self.qmuibar_hasSetEffectForegroundColor = YES;// QMUITheme 切换时会重新赋值,所以可能出现本来就是 nil,还给它又赋值了 nil,这种场景不应该导致 hasSet 标志位改变,所以要把标志位的设置放在 if (valueChanged) 里 + [self qmuibar_updateEffect]; + } +} + +- (UIColor *)qmui_effectForegroundColor { + return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_effectForegroundColor); +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIVisualEffectView.h b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.h similarity index 70% rename from QMUI/QMUIKit/QMUIComponents/QMUIVisualEffectView.h rename to QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.h index 9617b7cf..3258f9bd 100644 --- a/QMUI/QMUIKit/QMUIComponents/QMUIVisualEffectView.h +++ b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.h @@ -1,22 +1,23 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ - + */ // -// QMUIVisualEffectView.h +// UITabBar+QMUIBarProtocol.h // QMUIKit // -// Created by QMUI Team on 2018/6/19. +// Created by molice on 2022/5/18. // #import +#import "QMUIBarProtocol.h" -@interface QMUIVisualEffectView : UIVisualEffectView - -@property(nonatomic, strong) UIColor *foregroundColor UI_APPEARANCE_SELECTOR; +NS_ASSUME_NONNULL_BEGIN +@interface UITabBar (QMUIBarProtocol) @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.m b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.m new file mode 100644 index 00000000..78f24c3f --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.m @@ -0,0 +1,119 @@ +/** + * 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. + */ +// +// UITabBar+QMUIBarProtocol.m +// QMUIKit +// +// Created by molice on 2022/5/18. +// + +#import "UITabBar+QMUIBarProtocol.h" +#import "QMUIBarProtocolPrivate.h" +#import "QMUICore.h" +#import "UIVisualEffectView+QMUI.h" +#import "NSArray+QMUI.h" + +@interface UITabBar () +@end + +@implementation UITabBar (QMUIBarProtocol) + +QMUISynthesizeBOOLProperty(qmuibar_hasSetEffect, setQmuibar_hasSetEffect) +QMUISynthesizeBOOLProperty(qmuibar_hasSetEffectForegroundColor, setQmuibar_hasSetEffectForegroundColor) + +BeginIgnoreClangWarning(-Wobjc-protocol-method-implementation) +- (void)qmuibar_updateEffect { + [self.qmui_effectViews enumerateObjectsUsingBlock:^(UIVisualEffectView * _Nonnull effectView, NSUInteger idx, BOOL * _Nonnull stop) { + if (self.qmuibar_hasSetEffect) { + // 这里对 iOS 13 不使用 UITabBarAppearance.backgroundEffect 来修改,是因为反正不管 iOS 10 还是 13,最终都是 setBackgroundEffects: 在起作用,而且不用 UITabBarAppearance 还可以规避与 UIAppearance 机制的冲突 + NSArray *effects = self.qmuibar_backgroundEffects; + [effectView qmui_performSelector:NSSelectorFromString(@"setBackgroundEffects:") withArguments:&effects, nil]; + } + if (self.qmuibar_hasSetEffectForegroundColor) { + effectView.qmui_foregroundColor = self.qmui_effectForegroundColor; + } + }]; +} +EndIgnoreClangWarning + +// UITabBar、UIVisualEffectView 都有一个私有的方法 backgroundEffects,当 UIVisualEffectView 应用于 UITabBar 场景时,磨砂的效果实际上被放在 backgroundEffects 内,而不是公开接口的 effect 属性里,这里为了方便,将 UITabBar (QMUI).effect 转成可用于 backgroundEffects 的数组 +- (NSArray *)qmuibar_backgroundEffects { + if (self.qmuibar_hasSetEffect) { + return self.qmui_effect ? @[self.qmui_effect] : nil; + } + return nil; +} + +#pragma mark - + +- (UIView *)qmui_backgroundView { + return [self qmui_valueForKey:@"_backgroundView"]; +} + +- (UIImageView *)qmui_shadowImageView { + // bar 在 init 完就可以获取到 backgroundView 和 shadowView,无需关心调用时机的问题 + return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView1"]; +} + +- (UIVisualEffectView *)qmui_effectView { + NSArray *visibleEffectViews = [self.qmui_effectViews qmui_filterWithBlock:^BOOL(UIVisualEffectView * _Nonnull item) { + return !item.hidden && item.alpha > 0.01 && item.superview; + }]; + return visibleEffectViews.lastObject; +} + +- (NSArray *)qmui_effectViews { + UIView *backgroundView = self.qmui_backgroundView; + NSMutableArray *result = NSMutableArray.new; + UIVisualEffectView *backgroundEffectView1 = [backgroundView valueForKey:@"_effectView1"]; + UIVisualEffectView *backgroundEffectView2 = [backgroundView valueForKey:@"_effectView2"]; + if (backgroundEffectView1) { + [result addObject:backgroundEffectView1]; + } + if (backgroundEffectView2) { + [result addObject:backgroundEffectView2]; + } + return result.count > 0 ? result : nil; +} + +static char kAssociatedObjectKey_effect; +- (void)setQmui_effect:(UIBlurEffect *)qmui_effect { + if (qmui_effect) { + [QMUIBarProtocolPrivate swizzleBarBackgroundViewIfNeeded]; + } + + BOOL valueChanged = self.qmui_effect != qmui_effect; + objc_setAssociatedObject(self, &kAssociatedObjectKey_effect, qmui_effect, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (valueChanged) { + self.qmuibar_hasSetEffect = YES;// QMUITheme 切换时会重新赋值,所以可能出现本来就是 nil,还给它又赋值了 nil,这种场景不应该导致 hasSet 标志位改变,所以要把标志位的设置放在 if (valueChanged) 里 + [self qmuibar_updateEffect]; + } +} + +- (UIBlurEffect *)qmui_effect { + return (UIBlurEffect *)objc_getAssociatedObject(self, &kAssociatedObjectKey_effect); +} + +static char kAssociatedObjectKey_effectForegroundColor; +- (void)setQmui_effectForegroundColor:(UIColor *)qmui_effectForegroundColor { + if (qmui_effectForegroundColor) { + [QMUIBarProtocolPrivate swizzleBarBackgroundViewIfNeeded]; + } + BOOL valueChanged = ![self.qmui_effectForegroundColor isEqual:qmui_effectForegroundColor]; + objc_setAssociatedObject(self, &kAssociatedObjectKey_effectForegroundColor, qmui_effectForegroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (valueChanged) { + self.qmuibar_hasSetEffectForegroundColor = YES;// QMUITheme 切换时会重新赋值,所以可能出现本来就是 nil,还给它又赋值了 nil,这种场景不应该导致 hasSet 标志位改变,所以要把标志位的设置放在 if (valueChanged) 里 + [self qmuibar_updateEffect]; + } +} + +- (UIColor *)qmui_effectForegroundColor { + return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_effectForegroundColor); +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUIStringPrivate.h b/QMUI/QMUIKit/UIKitExtensions/QMUIStringPrivate.h new file mode 100644 index 00000000..39c2f01b --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/QMUIStringPrivate.h @@ -0,0 +1,28 @@ +/** + * 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. + */ +// +// QMUIStringPrivate.h +// QMUIKit +// +// Created by molice on 2021/11/5. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface QMUIStringPrivate : NSObject + ++ (nullable id)substring:(id)aString avoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo; ++ (nullable id)substring:(id)aString avoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo; ++ (nullable id)substring:(id)aString avoidBreakingUpCharacterSequencesWithRange:(NSRange)range lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo; ++ (nullable id)string:(id)aString avoidBreakingUpCharacterSequencesByRemoveCharacterAtIndex:(NSUInteger)index; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUIStringPrivate.m b/QMUI/QMUIKit/UIKitExtensions/QMUIStringPrivate.m new file mode 100644 index 00000000..a3bbd368 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/QMUIStringPrivate.m @@ -0,0 +1,412 @@ +/** + * 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. + */ +// +// QMUIStringPrivate.m +// QMUIKit +// +// Created by molice on 2021/11/5. +// + +#import "QMUIStringPrivate.h" +#import +#import "QMUICore.h" +#import "NSString+QMUI.h" + +@implementation QMUIStringPrivate + ++ (NSUInteger)transformIndexToDefaultMode:(NSUInteger)index inString:(NSString *)string { + CGFloat strlength = 0.f; + NSUInteger i = 0; + for (i = 0; i < string.length; i++) { + unichar character = [string characterAtIndex:i]; + if (isascii(character)) { + strlength += 1; + } else { + strlength += 2; + } + if (strlength >= index + 1) return i; + } + return 0; +} + ++ (NSRange)transformRangeToDefaultMode:(NSRange)range lessValue:(BOOL)lessValue inString:(NSString *)string { + CGFloat strlength = 0.f; + NSRange resultRange = NSMakeRange(NSNotFound, 0); + NSUInteger i = 0; + for (i = 0; i < string.length; i++) { + unichar character = [string characterAtIndex:i]; + if (isascii(character)) { + strlength += 1; + } else { + strlength += 2; + } + if ((lessValue && isascii(character) && strlength >= range.location + 1) + || (lessValue && !isascii(character) && strlength > range.location + 1) + || (!lessValue && strlength >= range.location + 1)) { + if (resultRange.location == NSNotFound) { + resultRange.location = i; + } + + if (range.length > 0 && strlength >= NSMaxRange(range)) { + resultRange.length = i - resultRange.location; + if (lessValue && (strlength == NSMaxRange(range))) { + resultRange.length += 1;// 尽量不包含字符的,只有在精准等于时才+1,否则就不算这最后一个字符 + } else if (!lessValue) { + resultRange.length += 1;// 只要是最大能力包含字符的,一进来就+1 + } + return resultRange; + } + } + } + return resultRange; +} + ++ (NSRange)downRoundRangeOfComposedCharacterSequences:(NSRange)range inString:(NSString *)string { + if (range.length == 0) { + return range; + } + NSRange systemRange = [string rangeOfComposedCharacterSequencesForRange:range];// 系统总是往大取值 + if (NSEqualRanges(range, systemRange)) { + return range; + } + NSRange result = systemRange; + if (range.location > systemRange.location) { + // 意味着传进来的 range 起点刚好在某个 Character Sequence 中间,所以要把这个 Character Sequence 遗弃掉,从它后面的字符开始算 + NSRange beginRange = [string rangeOfComposedCharacterSequenceAtIndex:range.location]; + result.location = NSMaxRange(beginRange); + result.length -= beginRange.length; + } + if (NSMaxRange(range) < NSMaxRange(systemRange)) { + // 意味着传进来的 range 终点刚好在某个 Character Sequence 中间,所以要把这个 Character Sequence 遗弃掉,只取到它前面的字符 + NSRange endRange = [string rangeOfComposedCharacterSequenceAtIndex:NSMaxRange(range) - 1]; + + // 如果参数传进来的 range 刚好落在一个 emoji 的中间,就会导致前面减完 beginRange 这里又减掉一个 endRange,出现负数(注意这里 length 是 NSUInteger),所以做个保护,可以用 👨‍👩‍👧‍👦 测试,这个 emoji 长度是 11 + if (result.length >= endRange.length) { + result.length = result.length - endRange.length; + } else { + result.length = 0; + } + } + return result; +} + ++ (id)substring:(id)aString avoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { + NSAttributedString *attributedString = [aString isKindOfClass:NSAttributedString.class] ? (NSAttributedString *)aString : nil; + NSString *string = attributedString.string ?: (NSString *)aString; + NSUInteger length = countingNonASCIICharacterAsTwo ? string.qmui_lengthWhenCountingNonASCIICharacterAsTwo : string.length; + QMUIAssert(index <= length, @"QMUIStringPrivate", @"%s, index %@ out of bounds. string = %@", __func__, @(index), attributedString ?: string); + if (index >= length) { + if (attributedString) { + return [[attributedString.class alloc] init]; + } + return @""; + }; + index = countingNonASCIICharacterAsTwo ? [self transformIndexToDefaultMode:index inString:string] : index;// 实际计算都按照系统默认的 length 规则来 + NSRange range = [string rangeOfComposedCharacterSequenceAtIndex:index]; + index = range.length == 1 ? index : (lessValue ? NSMaxRange(range) : range.location); + if (attributedString) { + NSAttributedString *resultString = [attributedString attributedSubstringFromRange:NSMakeRange(index, string.length - index)]; + return resultString; + } + NSString *resultString = [string substringFromIndex:index]; + return resultString; +} + ++ (id)substring:(id)aString avoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { + NSAttributedString *attributedString = [aString isKindOfClass:NSAttributedString.class] ? (NSAttributedString *)aString : nil; + NSString *string = attributedString.string ?: (NSString *)aString; + NSUInteger length = countingNonASCIICharacterAsTwo ? string.qmui_lengthWhenCountingNonASCIICharacterAsTwo : string.length; + QMUIAssert(index <= length, @"QMUIStringPrivate", @"%s, index %@ out of bounds. string = %@", __func__, @(index), attributedString ?: string); + if (index == 0 || index > length) { + if (attributedString) { + return [[attributedString.class alloc] init]; + } + return @""; + } + if (index == length) {// 根据系统 -[NSString substringToIndex:] 的注释,在 index 等于 length 时会返回 self 的 copy。 + if (attributedString) { + if ([attributedString isKindOfClass:NSMutableAttributedString.class]) { + return [aString mutableCopy]; + } + } + return [aString copy]; + } + index = countingNonASCIICharacterAsTwo ? [self transformIndexToDefaultMode:index inString:string] : index;// 实际计算都按照系统默认的 length 规则来 + NSRange range = [string rangeOfComposedCharacterSequenceAtIndex:index]; + index = range.length == 1 ? index : (lessValue ? range.location : NSMaxRange(range)); + if (attributedString) { + NSAttributedString *resultString = [attributedString attributedSubstringFromRange:NSMakeRange(0, index)]; + return resultString; + } + NSString *resultString = [string substringToIndex:index]; + return resultString; +} + ++ (id)substring:(id)aString avoidBreakingUpCharacterSequencesWithRange:(NSRange)range lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { + NSAttributedString *attributedString = [aString isKindOfClass:NSAttributedString.class] ? (NSAttributedString *)aString : nil; + NSString *string = attributedString.string ?: (NSString *)aString; + NSUInteger length = countingNonASCIICharacterAsTwo ? string.qmui_lengthWhenCountingNonASCIICharacterAsTwo : string.length; + QMUIAssert(NSMaxRange(range) <= length, @"QMUIStringPrivate", @"%s, range %@ out of bounds. string = %@", __func__, NSStringFromRange(range), attributedString ?: string); + if (NSMaxRange(range) > length) { + if (attributedString) { + return [[attributedString.class alloc] init]; + } + return @""; + } + range = countingNonASCIICharacterAsTwo ? [self transformRangeToDefaultMode:range lessValue:lessValue inString:string] : range;// 实际计算都按照系统默认的 length 规则来 + NSRange characterSequencesRange = lessValue ? [self downRoundRangeOfComposedCharacterSequences:range inString:string] : [string rangeOfComposedCharacterSequencesForRange:range]; + if (attributedString) { + NSAttributedString *resultString = [attributedString attributedSubstringFromRange:characterSequencesRange]; + return resultString; + } + NSString *resultString = [string substringWithRange:characterSequencesRange]; + return resultString; +} + ++ (id)string:(id)aString avoidBreakingUpCharacterSequencesByRemoveCharacterAtIndex:(NSUInteger)index { + NSAttributedString *attributedString = [aString isKindOfClass:NSAttributedString.class] ? (NSAttributedString *)aString : nil; + NSString *string = attributedString.string ?: (NSString *)aString; + NSRange rangeForRemove = [string rangeOfComposedCharacterSequenceAtIndex:index]; + if (attributedString) { + NSMutableAttributedString *resultString = attributedString.mutableCopy; + [resultString replaceCharactersInRange:rangeForRemove withString:@""]; + return resultString.copy; + } + NSString *resultString = [string stringByReplacingCharactersInRange:rangeForRemove withString:@""]; + return resultString; +} + +@end + +@implementation QMUIStringPrivate (Safety) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self qmuisafety_UIKeyboardImpl]; + [self qmuisafety_NSRegularExpression]; + [self qmuisafety_NSString]; + [self qmuisafety_NSAttributedString]; + }); +} + +static BOOL QMUIAvoidSubstring = NO; ++ (void)qmuisafety_UIKeyboardImpl { + // UIKeyboardImpl + // - (void) handleKeyWithString:(id)arg1 forKeyEvent:(id)arg2 executionContext:(id)arg3; + OverrideImplementation(NSClassFromString([NSString qmui_stringByConcat:@"UIKeyb", @"oard", @"Impl", nil]), NSSelectorFromString([NSString qmui_stringByConcat:@"handleKeyWithString:", @"forKeyEvent:", @"executionContext:", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(NSObject *selfObject, NSString *string, UIPressesEvent *event, NSObject *context) { + + QMUIAvoidSubstring = YES; + + // call super + void (*originSelectorIMP)(id, SEL, NSString *, UIPressesEvent *, NSObject *); + originSelectorIMP = (void (*)(id, SEL, id, id, id))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, string, event, context); + + QMUIAvoidSubstring = NO; + }; + }); +} + ++ (void)qmuisafety_NSRegularExpression { + // 避免 stringByReplacingMatchesInString 无效 + // https://github.com/Tencent/QMUI_iOS/issues/1542 + // -[NSRegularExpression(NSReplacement) stringByReplacingMatchesInString:options:range:withTemplate:] + // - (id) stringByReplacingMatchesInString:(id)arg1 options:(unsigned long)arg2 range:(struct _NSRange)arg3 withTemplate:(id)arg4; + OverrideImplementation([NSRegularExpression class], @selector(stringByReplacingMatchesInString:options:range:withTemplate:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^NSString *(NSRegularExpression *selfObject, NSString *string, NSMatchingOptions options, NSRange range, NSString *templ) { + + QMUIAvoidSubstring = YES; + + // call super + NSString * (*originSelectorIMP)(id, SEL, NSString *, NSMatchingOptions, NSRange, NSString *); + originSelectorIMP = (NSString * (*)(id, SEL, NSString *, NSMatchingOptions, NSRange, NSString *))originalIMPProvider(); + NSString * result = originSelectorIMP(selfObject, originCMD, string, options, range, templ); + + QMUIAvoidSubstring = NO; + + return result; + }; + }); +} + ++ (void)qmuisafety_NSString { + OverrideImplementation([NSString class], @selector(substringFromIndex:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^NSString *(NSString *selfObject, NSUInteger index) { + + // index 越界 + { + BOOL isValidatedIndex = index <= selfObject.length; + if (!isValidatedIndex) { + NSString *logString = [NSString stringWithFormat:@"%@ 传入了一个超过字符串长度的 index: %@,原字符串为: %@(%@)", NSStringFromSelector(originCMD), @(index), selfObject, @(selfObject.length)]; + QMUIAssert(NO, @"QMUIStringSafety", @"%@", logString); + return @"";// 系统 substringFromIndex: 返回值的标志是 nonnull + } + } + + // 保护从 emoji 等 ComposedCharacterSequence 中间裁剪的场景 + // 系统 emoji 键盘输入过程中一定会调用 substringFromIndex:text.length - 1,导致触发我们这个警告,这里特殊保护一下 + { + if (index < selfObject.length && !QMUIAvoidSubstring) { + NSRange range = [selfObject rangeOfComposedCharacterSequenceAtIndex:index]; + BOOL isValidatedIndex = range.location == index || NSMaxRange(range) == index; + if (!isValidatedIndex) { + NSString *logString = [NSString stringWithFormat:@"试图在 ComposedCharacterSequence 中间用 %@ 裁剪字符串,可能导致乱码、crash。原字符串为“%@”(%@),index 为 %@,命中的 ComposedCharacterSequence range 为 %@", NSStringFromSelector(originCMD), selfObject, @(selfObject.length), @(index), NSStringFromRange(range)]; + QMUIAssert(NO, @"QMUIStringSafety", @"%@", logString); + index = range.location; + } + } + } + + // call super + NSString * (*originSelectorIMP)(id, SEL, NSUInteger); + originSelectorIMP = (NSString * (*)(id, SEL, NSUInteger))originalIMPProvider(); + NSString * result = originSelectorIMP(selfObject, originCMD, index); + + return result; + }; + }); + + OverrideImplementation([NSString class], @selector(substringToIndex:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^NSString *(NSString *selfObject, NSUInteger index) { + + // index 越界 + { + BOOL isValidatedIndex = index <= selfObject.length; + if (!isValidatedIndex) { + NSString *logString = [NSString stringWithFormat:@"%@ 传入了一个超过字符串长度的 index: %@,原字符串为: %@(%@)", NSStringFromSelector(originCMD), @(index), selfObject, @(selfObject.length)]; + QMUIAssert(NO, @"QMUIStringSafety", @"%@", logString); + return @"";// 系统 substringToIndex: 返回值的标志是 nonnull,但返回 nil 比返回 @"" 更安全 + } + } + + // 保护从 emoji 等 ComposedCharacterSequence 中间裁剪的场景 + { + if (index < selfObject.length) { + NSRange range = [selfObject rangeOfComposedCharacterSequenceAtIndex:index]; + BOOL isValidatedIndex = range.location == index; + if (!isValidatedIndex) { + NSString *logString = [NSString stringWithFormat:@"试图在 ComposedCharacterSequence 中间用 %@ 裁剪字符串,可能导致乱码、crash。原字符串为“%@”(%@),index 为 %@,命中的 ComposedCharacterSequence range 为 %@", NSStringFromSelector(originCMD), selfObject, @(selfObject.length), @(index), NSStringFromRange(range)]; + QMUIAssert(NO, @"QMUIStringSafety", @"%@", logString); + index = range.location; + } + } + } + + // call super + NSString * (*originSelectorIMP)(id, SEL, NSUInteger); + originSelectorIMP = (NSString * (*)(id, SEL, NSUInteger))originalIMPProvider(); + NSString * result = originSelectorIMP(selfObject, originCMD, index); + + return result; + }; + }); + + + // 继承关系是 __NSCFConstantString → __NSCFString → NSMutableString → NSString,其中 __NSCFString 重写了 substringWithRange:(其他 substring 方法没任何人重写),所以这里要 hook __NSCFString 而不是 NSString + OverrideImplementation(NSClassFromString(@"__NSCFString"), @selector(substringWithRange:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^NSString *(NSString *selfObject, NSRange range) { + // range 越界,注意这里识别不了负值,例如一个 (10, -8) 的 range,它的 NSMaxRange 返回2,会认为长度小于 length 所以合法,但实际上是非法的,所以交给下面的流程专门识别。 + { + BOOL isValidddatedRange = NSMaxRange(range) <= selfObject.length; + if (!isValidddatedRange) { + NSString *logString = [NSString stringWithFormat:@"%@ 传入了一个超过字符串长度的 range: %@,原字符串为: %@(%@)", NSStringFromSelector(originCMD), NSStringFromRange(range), selfObject, @(selfObject.length)]; + QMUIAssert(NO, @"QMUIStringSafety", @"%@", logString); + return @"";// 系统 substringWithRange: 返回值的标志是 nonnull + } + } + + // rang 负值 + { + NSInteger location = range.location; + NSInteger length = range.length; + if (location < 0 || length < 0) { + NSString *logString = [NSString stringWithFormat:@"%@ 传入了一个可能由负数转换过来的 range: %@,猜测转换前数值为 (%@, %@),原字符串为: %@(%@)", NSStringFromSelector(originCMD), NSStringFromRange(range), @(location), @(length), selfObject, @(selfObject.length)]; + QMUIAssert(NO, @"QMUIStringSafety", @"%@", logString); +// return @"";// 由于理论上不可能准确识别这种情况,所以这里不干预 return 值,只是做个 assert 提醒 + } + } + + // 保护从 emoji 等 ComposedCharacterSequence 中间裁剪的场景 + { + if (NSMaxRange(range) < selfObject.length) { + NSRange range2 = [selfObject rangeOfComposedCharacterSequencesForRange:range]; + BOOL isValidddatedRange = range.length == 0 || NSEqualRanges(range, range2); + if (!isValidddatedRange && !QMUIAvoidSubstring) { + NSString *logString = [NSString stringWithFormat:@"试图在 ComposedCharacterSequence 中间用 %@ 裁剪字符串,可能导致乱码、crash。原字符串为“%@”(%@),range 为 %@,命中的 ComposedCharacterSequence range 为 %@", NSStringFromSelector(originCMD), selfObject, @(selfObject.length), NSStringFromRange(range), NSStringFromRange(range2)]; + QMUIAssert(NO, @"QMUIStringSafety", @"%@", logString); + range = range2; + } + } + } + + // call super + NSString * (*originSelectorIMP)(id, SEL, NSRange); + originSelectorIMP = (NSString * (*)(id, SEL, NSRange))originalIMPProvider(); + NSString * result = originSelectorIMP(selfObject, originCMD, range); + + 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 { + id (^initWithStringBlock)(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) = ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^id (id selfObject, NSString *str) { + + str = str ?: @""; + + // call super + id(*originSelectorIMP)(id, SEL, NSString *); + originSelectorIMP = (id (*)(id, SEL, NSString *))originalIMPProvider(); + id result = originSelectorIMP(selfObject, originCMD, str); + + return result; + }; + }; + + id (^initWithStringAttributesBlock)(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) = ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^id (id selfObject, NSString *str, NSDictionary *attrs) { + + str = str ?: @""; + + // call super + id(*originSelectorIMP)(id, SEL, NSString *, NSDictionary *); + originSelectorIMP = (id (*)(id, SEL, NSString *, NSDictionary *))originalIMPProvider(); + id result = originSelectorIMP(selfObject, originCMD, str, attrs); + + return result; + }; + }; + + // 类簇对不同的 init 方法对应不同的私有 class,所以要用实例来得到真正的class + OverrideImplementation([[[NSAttributedString alloc] initWithString:@""] class], @selector(initWithString:), initWithStringBlock); + OverrideImplementation([[[NSMutableAttributedString alloc] initWithString:@""] class], @selector(initWithString:), initWithStringBlock); + OverrideImplementation([[[NSAttributedString alloc] initWithString:@"" attributes:nil] class], @selector(initWithString:attributes:), initWithStringAttributesBlock); + OverrideImplementation([[[NSMutableAttributedString alloc] initWithString:@"" attributes:nil] class], @selector(initWithString:attributes:), initWithStringAttributesBlock); +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h index d3414b9b..9735c50d 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIActivityIndicatorView+QMUI.h @@ -15,18 +15,16 @@ #import -@interface UIActivityIndicatorView (QMUI) +NS_ASSUME_NONNULL_BEGIN /** - * 创建一个指定大小的UIActivityIndicatorView - * - * 系统的UIActivityIndicatorView尺寸是由UIActivityIndicatorViewStyle决定的,固定不变。因此创建后通过CGAffineTransformMakeScale将其缩放到指定大小。self.frame获取的值也是缩放后的值,不影响布局。 - * - * @param style UIActivityIndicatorViewStyle - * @param size UIActivityIndicatorView的大小 - * - * @return UIActivityIndicatorView对象 + 内部通过重写系统方法来让 UIActivityIndicatorView 支持 setFrame: 方式修改尺寸,业务就像使用一个普通 UIView 一样去使用它即可。 */ -- (instancetype)initWithActivityIndicatorStyle:(UIActivityIndicatorViewStyle)style size:(CGSize)size; +@interface UIActivityIndicatorView (QMUI) + +/// 内部转圈的那个 imageView +@property(nonatomic, strong, readonly) UIImageView *qmui_animatingView; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.m index 510797e4..128cdce1 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIActivityIndicatorView+QMUI.m @@ -14,16 +14,74 @@ // #import "UIActivityIndicatorView+QMUI.h" +#import "UIView+QMUI.h" +#import "QMUICore.h" + +@interface UIActivityIndicatorView () +@property(nonatomic, assign) CGSize qmuiai_size; +@end @implementation UIActivityIndicatorView (QMUI) -- (instancetype)initWithActivityIndicatorStyle:(UIActivityIndicatorViewStyle)style size:(CGSize)size { - if (self = [self initWithActivityIndicatorStyle:style]) { - CGSize initialSize = self.bounds.size; - CGFloat scale = size.width / initialSize.width; - self.transform = CGAffineTransformMakeScale(scale, scale); +QMUISynthesizeCGSizeProperty(qmuiai_size, setQmuiai_size) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + /** + 系统会在你调用 setFrame: 时把 loading 设置为你希望的 rect,但 sizeToFit 又回去了,所以这里需要通过重写 setFrame: 来记录希望的 size,在 sizeThatFits: 里返回。 + 另外内部的 animatingImageView 始终会保持默认大小,所以需要重写 layoutSubviews 让 animatingImageView 可改变尺寸。 + */ + OverrideImplementation([UIActivityIndicatorView class], @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIActivityIndicatorView *selfObject, CGRect firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, CGRect); + originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + selfObject.qmuiai_size = firstArgv.size; + }; + }); + + OverrideImplementation([UIActivityIndicatorView class], @selector(sizeThatFits:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGSize(UIActivityIndicatorView *selfObject, CGSize firstArgv) { + if (selfObject.qmuiai_size.width > 0) { + return selfObject.qmuiai_size; + } + + // call super + CGSize (*originSelectorIMP)(id, SEL, CGSize); + originSelectorIMP = (CGSize (*)(id, SEL, CGSize))originalIMPProvider(); + CGSize result = originSelectorIMP(selfObject, originCMD, firstArgv); + return result; + }; + }); + + OverrideImplementation([UIActivityIndicatorView class], @selector(layoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIActivityIndicatorView *selfObject) { + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + if (selfObject.qmuiai_size.width > 0) { + selfObject.qmui_animatingView.frame = selfObject.bounds; + } + }; + }); + }); +} + +- (UIImageView *)qmui_animatingView { + SEL sel = NSSelectorFromString(@"_animatingImageView"); + if ([self respondsToSelector:sel]) { + BeginIgnorePerformSelectorLeaksWarning + return [self performSelector:sel]; + EndIgnorePerformSelectorLeaksWarning } - return self; + return nil; } @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIApplication+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIApplication+QMUI.h new file mode 100644 index 00000000..94250659 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIApplication+QMUI.h @@ -0,0 +1,25 @@ +/** + * 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. + */ +// +// UIApplication+QMUI.h +// QMUIKit +// +// Created by MoLice on 2021/8/30. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIApplication (QMUI) + +/// 判断当前的 App 是否已经完全启动 +@property(nonatomic, assign, readonly) BOOL qmui_didFinishLaunching; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIApplication+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIApplication+QMUI.m new file mode 100644 index 00000000..36129ad3 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIApplication+QMUI.m @@ -0,0 +1,48 @@ +/** + * 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. + */ +// +// UIApplication+QMUI.m +// QMUIKit +// +// Created by MoLice on 2021/8/30. +// + +#import "UIApplication+QMUI.h" +#import "QMUICore.h" + +@implementation UIApplication (QMUI) + +QMUISynthesizeBOOLProperty(qmui_didFinishLaunching, setQmui_didFinishLaunching) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation(object_getClass(UIApplication.class), @selector(sharedApplication), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIApplication *(UIApplication *selfObject) { + // call super + UIApplication * (*originSelectorIMP)(id, SEL); + originSelectorIMP = (UIApplication * (*)(id, SEL))originalIMPProvider(); + UIApplication * result = originSelectorIMP(selfObject, originCMD); + + if (![result qmui_getBoundBOOLForKey:@"QMUIAddedObserver"]) { + [NSNotificationCenter.defaultCenter addObserver:result selector:@selector(qmui_handleDidFinishLaunchingNotification:) name:UIApplicationDidFinishLaunchingNotification object:nil]; + [result qmui_bindBOOL:YES forKey:@"QMUIAddedObserver"]; + } + + return result; + }; + }); + }); +} + +- (void)qmui_handleDidFinishLaunchingNotification:(NSNotification *)notification { + self.qmui_didFinishLaunching = YES; + [NSNotificationCenter.defaultCenter removeObserver:self name:UIApplicationDidFinishLaunchingNotification object:nil]; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIBarItem+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIBarItem+QMUI.h index 99107a68..adeb5efe 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIBarItem+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIBarItem+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIBarItem+QMUI.h diff --git a/QMUI/QMUIKit/UIKitExtensions/UIBarItem+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIBarItem+QMUI.m index 74cb505e..4812e800 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIBarItem+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIBarItem+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIBarItem+QMUI.m @@ -16,7 +16,6 @@ #import "UIBarItem+QMUI.h" #import "QMUICore.h" #import "UIView+QMUI.h" -#import "QMUIWeakObjectContainer.h" @interface UIBarItem () @@ -25,58 +24,27 @@ @interface UIBarItem () @implementation UIBarItem (QMUI) -// 用于某些低版本 iOS 里,在 UINavigationButton/UIToolbarButton/UITabBarButton 里建立对 UIBarItem 的引用 -static char kAssociatedObjectKey_referenceItem; - + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - // UIBarButtonItem -setView: + // -[UIBarButtonItem setView:] // @warning 如果作为 UIToolbar.items 使用,则 customView 的情况下,iOS 10 及以下的版本不会调用 setView:,所以那种情况改为在 setToolbarItems:animated: 时调用,代码见下方 ExtendImplementationOfVoidMethodWithSingleArgument([UIBarButtonItem class], @selector(setView:), UIView *, ^(UIBarButtonItem *selfObject, UIView *firstArgv) { [UIBarItem setView:firstArgv inBarButtonItem:selfObject]; }); - if (IOS_VERSION_NUMBER < 110000) { - // iOS 11.0 及以上,通过 setView: 调用 qmui_viewDidSetBlock 即可,10.0 及以下只能在 setToolbarItems 的时机触发 - ExtendImplementationOfVoidMethodWithTwoArguments([UIViewController class], @selector(setToolbarItems:animated:), NSArray<__kindof UIBarButtonItem *> *, BOOL, ^(UIViewController *selfObject, NSArray<__kindof UIBarButtonItem *> *firstArgv, BOOL secondArgv) { - for (UIBarButtonItem *item in firstArgv) { - [UIBarItem setView:item.customView inBarButtonItem:item]; - } - }); - } - - - // UITabBarItem -setView: + // -[UITabBarItem setView:] ExtendImplementationOfVoidMethodWithSingleArgument([UITabBarItem class], @selector(setView:), UIView *, ^(UITabBarItem *selfObject, UIView *firstArgv) { [UIBarItem setView:firstArgv inBarItem:selfObject]; }); - - void (^layoutSubviewsBlock)(UIView *selfObject) = ^void(UIView *selfObject) { - UIBarItem *item = (UIBarItem *)((QMUIWeakObjectContainer *)objc_getAssociatedObject(self, &kAssociatedObjectKey_referenceItem)).object; - if (item.qmui_viewDidLayoutSubviewsBlock) { - item.qmui_viewDidLayoutSubviewsBlock(item, selfObject); - } - }; - - // iOS 10 及以下,UIBarButtonItem 的 view 的 layoutSubviews 没有调用 super,所以无法利用 UIView (QMUI).qmui_layoutSubviewsBlock 实现这个功能,所以这里才需要直接重写该 class 的 layoutSubviews - if (IOS_VERSION_NUMBER < 110000) { - ExtendImplementationOfVoidMethodWithoutArguments(NSClassFromString([NSString stringWithFormat:@"%@%@", @"UINavigation", @"Button"]), @selector(layoutSubviews), layoutSubviewsBlock); - ExtendImplementationOfVoidMethodWithoutArguments(NSClassFromString([NSString stringWithFormat:@"%@%@", @"UIToolbar", @"Button"]), @selector(layoutSubviews), layoutSubviewsBlock); - } - - // iOS 9 及以下,UITabBarItem 的 view 的 layoutSubviews 没有调用 super,所以无法利用 UIView (QMUI).qmui_layoutSubviewsBlock 实现这个功能,所以这里才需要直接重写该 class 的 layoutSubviews - if (IOS_VERSION_NUMBER < 100000) { - ExtendImplementationOfVoidMethodWithoutArguments(NSClassFromString([NSString stringWithFormat:@"%@%@", @"UITab", @"BarButton"]), @selector(layoutSubviews), layoutSubviewsBlock); - } }); } - (UIView *)qmui_view { // UIBarItem 本身没有 view 属性,只有子类 UIBarButtonItem 和 UITabBarItem 才有 if ([self respondsToSelector:@selector(view)]) { - return [self valueForKey:@"view"]; + return [self qmui_valueForKey:@"view"]; } return nil; } @@ -106,7 +74,7 @@ - (void)setQmui_viewLayoutDidChangeBlock:(void (^)(__kindof UIBarItem * _Nonnull objc_setAssociatedObject(self, &kAssociatedObjectKey_viewLayoutDidChangeBlock, qmui_viewLayoutDidChangeBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); // 这里有个骚操作,对于 iOS 11 及以上,item.view 被放在一个 UIStackView 内,而当屏幕旋转时,通过 item.view.qmui_frameDidChangeBlock 得到的时机过早,布局尚未被更新,所以把 qmui_frameDidChangeBlock 放到 stackView 上以保证时机的准确性,但当调用 qmui_viewLayoutDidChangeBlock 时传进去的参数 view 依然要是 item.view UIView *view = self.qmui_view; - if (IOS_VERSION_NUMBER >= 110000 && [view.superview isKindOfClass:[UIStackView class]]) { + if ([view.superview isKindOfClass:[UIStackView class]]) { view = self.qmui_view.superview; } if (view) { @@ -130,28 +98,6 @@ + (NSString *)identifierWithView:(UIView *)view block:(id)block { } + (void)setView:(UIView *)view inBarItem:(__kindof UIBarItem *)item { - if (IOS_VERSION_NUMBER < 110000) { - if ([NSStringFromClass(view.class) hasPrefix:@"UINavigation"] || [NSStringFromClass(view.class) hasPrefix:@"UIToolbar"]) { - QMUIWeakObjectContainer *weakContainer = objc_getAssociatedObject(self, &kAssociatedObjectKey_referenceItem); - if (!weakContainer) { - weakContainer = [QMUIWeakObjectContainer new]; - } - weakContainer.object = item; - objc_setAssociatedObject(view, &kAssociatedObjectKey_referenceItem, weakContainer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - } - - if (IOS_VERSION_NUMBER < 100000) { - if ([NSStringFromClass(view.class) hasPrefix:@"UITabBar"]) { - QMUIWeakObjectContainer *weakContainer = objc_getAssociatedObject(self, &kAssociatedObjectKey_referenceItem); - if (!weakContainer) { - weakContainer = [QMUIWeakObjectContainer new]; - } - weakContainer.object = item; - objc_setAssociatedObject(view, &kAssociatedObjectKey_referenceItem, weakContainer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - } - if (item.qmui_viewDidSetBlock) { item.qmui_viewDidSetBlock(item, view); } diff --git a/QMUI/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.h index 296ae81f..deae2f70 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIBezierPath+QMUI.h @@ -16,6 +16,8 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + @interface UIBezierPath (QMUI) /** @@ -24,5 +26,12 @@ * @param cornerRadius 圆角大小的数字,长度必须为4,顺序分别为[左上角、左下角、右下角、右上角] * @param lineWidth 描边的大小,如果不需要描边(例如path是用于fill而不是用于stroke),则填0 */ -+ (UIBezierPath *)qmui_bezierPathWithRoundedRect:(CGRect)rect cornerRadiusArray:(NSArray *)cornerRadius lineWidth:(CGFloat)lineWidth; ++ (instancetype)qmui_bezierPathWithRoundedRect:(CGRect)rect cornerRadiusArray:(NSArray *)cornerRadius lineWidth:(CGFloat)lineWidth; + +/** + 创建一条尺寸为[0,1]的正方形区域内的曲线,曲线由 CAMediaTimingFunction 转换而来。如果希望得到不同尺寸的 path,请通过 -[UIBezierPath applyTransform:] 转换。 + */ ++ (instancetype)qmui_bezierPathWithMediaTimingFunction:(CAMediaTimingFunction *)function; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.m index 94380c98..afff2f02 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIBezierPath+QMUI.m @@ -17,14 +17,15 @@ @implementation UIBezierPath (QMUI) -+ (UIBezierPath *)qmui_bezierPathWithRoundedRect:(CGRect)rect cornerRadiusArray:(NSArray *)cornerRadius lineWidth:(CGFloat)lineWidth { ++ (instancetype)qmui_bezierPathWithRoundedRect:(CGRect)rect cornerRadiusArray:(NSArray *)cornerRadius lineWidth:(CGFloat)lineWidth { + NSAssert(cornerRadius.count == 4, @"cornerRadiusArray.count should be 4."); CGFloat topLeftCornerRadius = cornerRadius[0].floatValue; CGFloat bottomLeftCornerRadius = cornerRadius[1].floatValue; CGFloat bottomRightCornerRadius = cornerRadius[2].floatValue; CGFloat topRightCornerRadius = cornerRadius[3].floatValue; CGFloat lineCenter = lineWidth / 2.0; - UIBezierPath *path = [UIBezierPath bezierPath]; + UIBezierPath *path = [self bezierPath]; [path moveToPoint:CGPointMake(topLeftCornerRadius, lineCenter)]; [path addArcWithCenter:CGPointMake(topLeftCornerRadius, topLeftCornerRadius) radius:topLeftCornerRadius - lineCenter startAngle:M_PI * 1.5 endAngle:M_PI clockwise:NO]; [path addLineToPoint:CGPointMake(lineCenter, CGRectGetHeight(rect) - bottomLeftCornerRadius)]; @@ -38,4 +39,14 @@ + (UIBezierPath *)qmui_bezierPathWithRoundedRect:(CGRect)rect cornerRadiusArray: return path; } ++ (instancetype)qmui_bezierPathWithMediaTimingFunction:(CAMediaTimingFunction *)function { + float point1[2], point2[2]; + [function getControlPointAtIndex:1 values:(float *)&point1]; + [function getControlPointAtIndex:2 values:(float *)&point2]; + UIBezierPath *path = [self bezierPath]; + [path moveToPoint:CGPointZero]; + [path addCurveToPoint:CGPointMake(1, 1) controlPoint1:CGPointMake(point1[0], point1[1]) controlPoint2:CGPointMake(point2[0], point2[1])]; + return path; +} + @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIBlurEffect+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIBlurEffect+QMUI.h new file mode 100644 index 00000000..4f73e5ea --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIBlurEffect+QMUI.h @@ -0,0 +1,33 @@ +/** + * 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. + */ +// +// UIBlurEffect+QMUI.h +// QMUIKit +// +// Created by MoLice on 2021/N/25. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIBlurEffect (QMUI) + +/** + 创建一个指定模糊半径的磨砂效果,注意这种方式创建的磨砂对象的 style 属性是无意义的(可以理解为系统的磨砂有两个维度:style、radius)。 + */ ++ (instancetype)qmui_effectWithBlurRadius:(CGFloat)radius; + +/** + 获取当前 UIBlurEffect 的 style,前提是该 UIBlurEffect 对象是通过 effectWithStyle: 方式创建的。如果是通过指定 radius 方式创建的,则 qmui_style 会返回一个无意义的值。 + */ +@property(nonatomic, assign, readonly) UIBlurEffectStyle qmui_style; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIBlurEffect+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIBlurEffect+QMUI.m new file mode 100644 index 00000000..fda8abda --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIBlurEffect+QMUI.m @@ -0,0 +1,33 @@ +/** + * 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. + */ +// +// UIBlurEffect+QMUI.m +// QMUIKit +// +// Created by MoLice on 2021/N/25. +// + +#import "UIBlurEffect+QMUI.h" +#import "QMUICore.h" + +@implementation UIBlurEffect (QMUI) + ++ (instancetype)qmui_effectWithBlurRadius:(CGFloat)radius { + // -[UIBlurEffect effectWithBlurRadius:] + UIBlurEffect *effect = [self qmui_performSelector:NSSelectorFromString(@"effectWithBlurRadius:") withArguments:&radius, nil]; + return effect; +} + +- (UIBlurEffectStyle)qmui_style { + UIBlurEffectStyle style; + // -[UIBlurEffect _style] + [self qmui_performSelector:NSSelectorFromString(@"_style") withPrimitiveReturnValue:&style]; + return style; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIButton+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIButton+QMUI.h index 9028440b..8ac38163 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIButton+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIButton+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIButton+QMUI.h @@ -15,30 +15,11 @@ #import -@interface UIButton (QMUI) - -typedef NS_ENUM(NSUInteger, QMUICustomizeButtonPropType) { - QMUICustomizeButtonPropTypeTitle, - QMUICustomizeButtonPropTypeTitleColor, - QMUICustomizeButtonPropTypeTitleShadowColor, - QMUICustomizeButtonPropTypeImage, - QMUICustomizeButtonPropTypeBackgroundImage, - QMUICustomizeButtonPropTypeAttributedTitle -}; +NS_ASSUME_NONNULL_BEGIN -- (instancetype)qmui_initWithImage:(UIImage *)image title:(NSString *)title; - -/** - * 判断该 button 在特定 UIControlState 下是否设置了属性 - * @note 该方法会对设置了任何 QMUICustomizeButtonPropType 都返回 YES - */ -- (BOOL)qmui_hasCustomizedButtonPropForState:(UIControlState)state; +@interface UIButton (QMUI) -/** - * 判断该 button 在特定 UIControlState 下是否设置了某个 QMUICustomizeButtonPropType 属性 - * @param type 对应于 UIbutton 的 setXXX:forState 办法 - */ -- (BOOL)qmui_hasCustomizedButtonPropWithType:(QMUICustomizeButtonPropType)type forState:(UIControlState)state; +- (instancetype)qmui_initWithImage:(nullable UIImage *)image title:(nullable NSString *)title; /** * 在UIButton的样式(如字体)设置完后,将button的text设置为一个测试字符,再调用sizeToFit,从而令button的高度适应字体 @@ -52,6 +33,16 @@ typedef NS_ENUM(NSUInteger, QMUICustomizeButtonPropType) { * @note 该方法和 setTitleColor:forState: 均可设置字体颜色,如果二者冲突,则代码顺序较后的方法定义的颜色会最终生效 * @note 如果包含了 NSKernAttributeName ,则此方法会自动帮你去掉最后一个字的 kern 效果,否则容易导致文字整体在视觉上不居中 */ -- (void)qmui_setTitleAttributes:(NSDictionary *)attributes forState:(UIControlState)state; +- (void)qmui_setTitleAttributes:(nullable NSDictionary *)attributes forState:(UIControlState)state; + +/** + 为指定 state 的图片设置颜色,当使用这个方法时,会用 Core Graphic 将该状态的图片渲染成指定颜色,并修改 renderingMode 为 UIImageRenderingModeAlwaysOriginal,会有一定性能负担,所以只适用于小图场景。 + @param color 图片的颜色,为 nil 则清空之前为该 state 指定的 imageTintColor + @param state 指定的状态 + @note 先 setImage 还是先 setImageTintColor,效果都是相同的 + */ +- (void)qmui_setImageTintColor:(nullable UIColor *)color forState:(UIControlState)state; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIButton+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIButton+QMUI.m index 5201c14d..0f81e8fe 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIButton+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIButton+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIButton+QMUI.m @@ -15,81 +15,74 @@ #import "UIButton+QMUI.h" #import "QMUICore.h" +#import "UIImage+QMUI.h" @interface UIButton () @property(nonatomic, strong) NSMutableDictionary *> *qbt_titleAttributes; -@property(nonatomic, strong) NSMutableDictionary *> * qbt_customizeButtonPropDict; +@property(nonatomic, strong) NSMutableSet *qbt_statesWithTitle; + +@property(nonatomic, strong) NSMutableDictionary *qbt_imageTintColors; +@property(nonatomic, strong) NSMutableSet *qbt_statesWithImageTintColor; @end @implementation UIButton (QMUI) QMUISynthesizeIdStrongProperty(qbt_titleAttributes, setQbt_titleAttributes) -QMUISynthesizeIdStrongProperty(qbt_customizeButtonPropDict, setQbt_customizeButtonPropDict) +QMUISynthesizeIdStrongProperty(qbt_statesWithTitle, setQbt_statesWithTitle) +QMUISynthesizeIdStrongProperty(qbt_imageTintColors, setQbt_imageTintColors) +QMUISynthesizeIdStrongProperty(qbt_statesWithImageTintColor, setQbt_statesWithImageTintColor) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ExtendImplementationOfVoidMethodWithTwoArguments([UIButton class], @selector(setTitle:forState:), NSString *, UIControlState, ^(UIButton *selfObject, NSString *title, UIControlState state) { - [selfObject _markQMUICustomizeType:QMUICustomizeButtonPropTypeTitle forState:state value:title]; - - if (!title || !selfObject.qbt_titleAttributes.count) { - return; - } - - if (state == UIControlStateNormal) { - [selfObject.qbt_titleAttributes enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, NSDictionary * _Nonnull obj, BOOL * _Nonnull stop) { - UIControlState state = [key unsignedIntegerValue]; - NSString *titleForState = [selfObject titleForState:state]; - NSAttributedString *string = [[NSAttributedString alloc] initWithString:titleForState attributes:obj]; - [selfObject setAttributedTitle:[selfObject attributedStringWithEndKernRemoved:string] forState:state]; - }]; - return; - } - - if ([selfObject.qbt_titleAttributes objectForKey:@(state)]) { - NSAttributedString *string = [[NSAttributedString alloc] initWithString:title attributes:selfObject.qbt_titleAttributes[@(state)]]; - [selfObject setAttributedTitle:[selfObject attributedStringWithEndKernRemoved:string] forState:state]; - return; - } + OverrideImplementation([UIButton class], @selector(setTitle:forState:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIButton *selfObject, NSString *title, UIControlState state) { + + if (title.length) { + if (!selfObject.qbt_statesWithTitle) { + selfObject.qbt_statesWithTitle = [[NSMutableSet alloc] init]; + } + if (state == UIControlStateNormal) { + [selfObject.qbt_statesWithTitle addObject:@(state)]; + } else { + NSString *normalTitle = [selfObject titleForState:UIControlStateNormal] ?: [selfObject attributedTitleForState:UIControlStateNormal].string; + if (![title isEqualToString:normalTitle]) { + [selfObject.qbt_statesWithTitle addObject:@(state)]; + } else { + [selfObject.qbt_statesWithTitle removeObject:@(state)]; + } + } + } else { + [selfObject.qbt_statesWithTitle removeObject:@(state)]; + } + + // call super + void (*originSelectorIMP)(id, SEL, NSString *, UIControlState); + originSelectorIMP = (void (*)(id, SEL, NSString *, UIControlState))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, title, state); + + [selfObject qbt_syncTitleByStates]; + }; }); - // 如果之前已经设置了此 state 下的文字颜色,则覆盖掉之前的颜色 - ExtendImplementationOfVoidMethodWithTwoArguments([UIButton class], @selector(setTitleColor:forState:), UIColor *, UIControlState, ^(UIButton *selfObject, UIColor *color, UIControlState state) { - [selfObject _markQMUICustomizeType:QMUICustomizeButtonPropTypeTitleColor forState:state value:color]; - - NSDictionary *attributes = selfObject.qbt_titleAttributes[@(state)]; - if (attributes) { - NSMutableDictionary *newAttributes = [NSMutableDictionary dictionaryWithDictionary:attributes]; - newAttributes[NSForegroundColorAttributeName] = color; - [selfObject qmui_setTitleAttributes:[NSDictionary dictionaryWithDictionary:newAttributes] forState:state]; + ExtendImplementationOfVoidMethodWithoutArguments([UIButton class], @selector(layoutSubviews), ^(UIButton *selfObject) { + // 临时解决 iOS 13 开启了粗体文本(Bold Text)导致 UIButton Title 显示不完整 https://github.com/Tencent/QMUI_iOS/issues/620 + if (UIAccessibilityIsBoldTextEnabled()) { + [selfObject.titleLabel sizeToFit]; } }); - - ExtendImplementationOfVoidMethodWithTwoArguments([UIButton class], @selector(setTitleShadowColor:forState:), UIColor *, UIControlState, ^(UIButton *selfObject, UIColor *color, UIControlState state) { - [selfObject _markQMUICustomizeType:QMUICustomizeButtonPropTypeTitleShadowColor forState:state value:color]; - }); - - ExtendImplementationOfVoidMethodWithTwoArguments([UIButton class], @selector(setImage:forState:), UIImage *, UIControlState, ^(UIButton *selfObject, UIImage *image, UIControlState state) { - [selfObject _markQMUICustomizeType:QMUICustomizeButtonPropTypeImage forState:state value:image]; - }); - - ExtendImplementationOfVoidMethodWithTwoArguments([UIButton class], @selector(setBackgroundImage:forState:), UIImage *, UIControlState, ^(UIButton *selfObject, UIImage *image, UIControlState state) { - [selfObject _markQMUICustomizeType:QMUICustomizeButtonPropTypeBackgroundImage forState:state value:image]; - }); - - ExtendImplementationOfVoidMethodWithTwoArguments([UIButton class], @selector(setAttributedTitle:forState:), NSAttributedString *, UIControlState, ^(UIButton *selfObject, NSAttributedString *title, UIControlState state) { - [selfObject _markQMUICustomizeType:QMUICustomizeButtonPropTypeAttributedTitle forState:state value:title]; - }); }); } - (instancetype)qmui_initWithImage:(UIImage *)image title:(NSString *)title { + // 非 init 开头的方法,无法给 self 赋值,所以无法像常规的写法一样 self = [self init] BeginIgnoreClangWarning(-Wunused-value) [self init]; EndIgnoreClangWarning + [self setImage:image forState:UIControlStateNormal]; [self setTitle:title forState:UIControlStateNormal]; return self; @@ -101,46 +94,36 @@ - (void)qmui_calculateHeightAfterSetAppearance { [self setTitle:nil forState:UIControlStateNormal]; } -- (BOOL)qmui_hasCustomizedButtonPropForState:(UIControlState)state { - if (self.qbt_customizeButtonPropDict) { - return self.qbt_customizeButtonPropDict[@(state)].count > 0; - } - - return NO; -} - -- (BOOL)qmui_hasCustomizedButtonPropWithType:(QMUICustomizeButtonPropType)type forState:(UIControlState)state { - if (self.qbt_customizeButtonPropDict && self.qbt_customizeButtonPropDict[@(state)]) { - return [self.qbt_customizeButtonPropDict[@(state)][@(type)] boolValue]; - } - - return NO; -} - -#pragma mark - Title Attributes +#pragma mark - TitleAttributes - (void)qmui_setTitleAttributes:(NSDictionary *)attributes forState:(UIControlState)state { - if (!attributes) { + if (!attributes && self.qbt_titleAttributes) { [self.qbt_titleAttributes removeObjectForKey:@(state)]; - [self setAttributedTitle:nil forState:state]; return; } + [UIButton qbt_swizzleForTitleAttributesIfNeeded]; + if (!self.qbt_titleAttributes) { - self.qbt_titleAttributes = [NSMutableDictionary dictionary]; + self.qbt_titleAttributes = [[NSMutableDictionary alloc] init]; } - // 如果传入的 attributes 没有包含文字颜色,则使用用户之前通过 setTitleColor:forState: 方法设置的颜色 - if (![attributes objectForKey:NSForegroundColorAttributeName]) { - NSMutableDictionary *newAttributes = [NSMutableDictionary dictionaryWithDictionary:attributes]; - newAttributes[NSForegroundColorAttributeName] = [self titleColorForState:state]; - attributes = [NSDictionary dictionaryWithDictionary:newAttributes]; + // 从 Normal 同步样式到其他 state + if (state != UIControlStateNormal && self.qbt_titleAttributes[@(UIControlStateNormal)]) { + NSMutableDictionary *temp = attributes.mutableCopy; + NSDictionary *normalAttributes = self.qbt_titleAttributes[@(UIControlStateNormal)]; + for (NSAttributedStringKey key in normalAttributes.allKeys) { + if (!temp[key]) { + temp[key] = normalAttributes[key]; + } + } + attributes = temp.copy; } + self.qbt_titleAttributes[@(state)] = attributes; // 确保调用此方法设置 attributes 之前已经通过 setTitle:forState: 设置的文字也能应用上新的 attributes - NSString *originalText = [self titleForState:state]; - [self setTitle:originalText forState:state]; + [self qbt_syncTitleByStates]; // 一个系统的不好的特性(bug?):如果你给 UIControlStateHighlighted(或者 normal 之外的任何 state)设置了包含 NSFont/NSKern/NSUnderlineAttributeName 之类的 attributedString ,但又仅用 setTitle:forState: 给 UIControlStateNormal 设置了普通的 string ,则按钮从 highlighted 切换回 normal 状态时,font 之类的属性依然会停留在 highlighted 时的状态 // 为了解决这个问题,我们要确保一旦有 normal 之外的 state 通过设置 qbt_titleAttributes 属性而导致使用了 attributedString,则 normal 也必须使用 attributedString @@ -149,44 +132,104 @@ - (void)qmui_setTitleAttributes:(NSDictionary *)attrib } } -// 去除最后一个字的 kern 效果 -- (NSAttributedString *)attributedStringWithEndKernRemoved:(NSAttributedString *)string { - if (!string || !string.length) { - return string; +// 如果 normal 用了 attributedTitle,那么其他的 state 都必须用 attributedTitle,否则就无法展示出来 +- (void)qbt_syncTitleByStates { + if (!self.qbt_titleAttributes.count) return; + for (NSNumber *stateValue in self.qbt_statesWithTitle) { + UIControlState state = stateValue.unsignedIntegerValue; + NSString *title = [self titleForState:state]; + NSDictionary *attributes = self.qbt_titleAttributes[stateValue] ?: self.qbt_titleAttributes[@(UIControlStateNormal)]; + NSAttributedString *string = [[NSAttributedString alloc] initWithString:title attributes:attributes]; + string = [UIButton qbt_attributedStringByRemovingLastKern:string]; + [self setAttributedTitle:string forState:state]; } - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:string]; - [attributedString removeAttribute:NSKernAttributeName range:NSMakeRange(string.length - 1, 1)]; - return [[NSAttributedString alloc] initWithAttributedString:attributedString]; } -#pragma mark - customize state ++ (void)qbt_swizzleForTitleAttributesIfNeeded { + [QMUIHelper executeBlock:^{ + // 如果之前已经设置了此 state 下的文字颜色,则覆盖掉之前的颜色 + OverrideImplementation([UIButton class], @selector(setTitleColor:forState:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIButton *selfObject, UIColor *color, UIControlState state) { + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *, UIControlState); + originSelectorIMP = (void (*)(id, SEL, UIColor *, UIControlState))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, color, state); + + NSDictionary *attributes = selfObject.qbt_titleAttributes[@(state)]; + if (attributes) { + NSMutableDictionary *newAttributes = attributes.mutableCopy; + newAttributes[NSForegroundColorAttributeName] = color; + [selfObject qmui_setTitleAttributes:newAttributes.copy forState:state]; + } + }; + }); + } oncePerIdentifier:@"UIButton (QMUI) titleAttributes"]; +} -- (void)_markQMUICustomizeType:(QMUICustomizeButtonPropType)type forState:(UIControlState)state value:(id)value { - if (value) { - [self _setQMUICustomizeType:type forState:state]; - } else { - [self _removeQMUICustomizeType:type forState:state]; +// 去除最后一个字的 kern 效果,避免字符串尾部出现多余的空白 ++ (NSAttributedString *)qbt_attributedStringByRemovingLastKern:(NSAttributedString *)string { + if (!string.length) { + return string; } + NSMutableAttributedString *attributedString = string.mutableCopy; + [attributedString removeAttribute:NSKernAttributeName range:NSMakeRange(string.length - 1, 1)]; + return attributedString.copy; } -- (void)_setQMUICustomizeType:(QMUICustomizeButtonPropType)type forState:(UIControlState)state { - if (!self.qbt_customizeButtonPropDict) { - self.qbt_customizeButtonPropDict = [NSMutableDictionary dictionary]; +#pragma mark - ImageTintColor + +- (void)qmui_setImageTintColor:(UIColor *)color forState:(UIControlState)state { + if (!color && self.qbt_imageTintColors) { + [self.qbt_imageTintColors removeObjectForKey:@(state)]; + return; } - if (!self.qbt_customizeButtonPropDict[@(state)]) { - self.qbt_customizeButtonPropDict[@(state)] = [NSMutableDictionary dictionary]; + [UIButton qbt_swizzleForImageTintColorIfNeeded]; + + if (!self.qbt_imageTintColors) { + self.qbt_imageTintColors = [[NSMutableDictionary alloc] init]; } + self.qbt_imageTintColors[@(state)] = color; - self.qbt_customizeButtonPropDict[@(state)][@(type)] = @(YES); + UIImage *stateImage = [self imageForState:state]; + if (!stateImage) return; + stateImage = [[stateImage qmui_imageWithTintColor:color] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + [self setImage:stateImage forState:state]; } -- (void)_removeQMUICustomizeType:(QMUICustomizeButtonPropType)type forState:(UIControlState)state { - if (!self.qbt_customizeButtonPropDict || !self.qbt_customizeButtonPropDict[@(state)]) { - return; - } - - [self.qbt_customizeButtonPropDict[@(state)] removeObjectForKey:@(type)]; ++ (void)qbt_swizzleForImageTintColorIfNeeded { + [QMUIHelper executeBlock:^{ + OverrideImplementation([UIButton class], @selector(setImage:forState:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIButton *selfObject, UIImage *image, UIControlState state) { + + BOOL isFirstSetImage = image && ![selfObject imageForState:UIControlStateNormal]; + + UIColor *imageTintColor = selfObject.qbt_imageTintColors[@(state)]; + if (imageTintColor) { + image = [[image qmui_imageWithTintColor:imageTintColor] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + } + + // call super + void (*originSelectorIMP)(id, SEL, UIImage *, UIControlState); + originSelectorIMP = (void (*)(id, SEL, UIImage *, UIControlState))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, image, state); + + if (isFirstSetImage) { + [selfObject.qbt_imageTintColors enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, UIColor * _Nonnull color, BOOL * _Nonnull stop) { + UIControlState s = key.unsignedIntegerValue; + if (s != state) {// 避免死循环 + UIImage *stateImage = [selfObject imageForState:s]; + if (stateImage) { + stateImage = [[stateImage qmui_imageWithTintColor:color] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + [selfObject setImage:stateImage forState:s]; + } + } + }]; + } + }; + }); + } oncePerIdentifier:@"UIButton (QMUI) titleAttributes"]; } @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UICollectionView+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UICollectionView+QMUI.h index 4ae36f7b..1a8a8528 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UICollectionView+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UICollectionView+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UICollectionView+QMUI.h diff --git a/QMUI/QMUIKit/UIKitExtensions/UICollectionView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UICollectionView+QMUI.m index c01317dd..2eb00f26 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UICollectionView+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UICollectionView+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UICollectionView+QMUI.m @@ -26,8 +26,9 @@ + (void)load { // 防止 release 版本滚动到不合法的 indexPath 会 crash OverrideImplementation([UICollectionView class], @selector(scrollToItemAtIndexPath:atScrollPosition:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UICollectionView *selfObject, NSIndexPath *indexPath, UICollectionViewScrollPosition scrollPosition, BOOL animated) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { + // UIDatePicker 每次点开都会先调用几次 indexPath 为 nil 的 scroll,屏蔽掉 + BOOL isUIKitClass = [NSStringFromClass(selfObject.superview.class) hasPrefix:@"_UIDatePickerCalendar"]; + if (!isUIKitClass) { BOOL isIndexPathLegal = YES; NSInteger numberOfSections = [selfObject numberOfSections]; if (indexPath.section >= numberOfSections) { @@ -39,10 +40,7 @@ + (void)load { } } if (!isIndexPathLegal) { - QMUILogWarn(@"UICollectionView (QMUI)", @"%@ - target indexPath : %@ ,不合法的indexPath。\n%@", selfObject, indexPath, [NSThread callStackSymbols]); - if (QMUICMIActivated && !ShouldPrintQMUIWarnLogToConsole) { - NSAssert(NO, @"出现不合法的indexPath"); - } + QMUIAssert(NO, @"UICollectionView (QMUI)", @"%@ - target indexPath : %@ ,不合法的indexPath。\n%@", selfObject, indexPath, [NSThread callStackSymbols]); return; } } diff --git a/QMUI/QMUIKit/UIKitExtensions/UICollectionViewCell+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UICollectionViewCell+QMUI.h new file mode 100644 index 00000000..a383534d --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UICollectionViewCell+QMUI.h @@ -0,0 +1,26 @@ +/** + * 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. + */ +// +// UICollectionViewCell+QMUI.h +// QMUIKit +// +// Created by MoLice on 2021/M/9. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UICollectionViewCell (QMUI) + +/// 设置 cell 点击时的背景色,如果没有 selectedBackgroundView 会创建一个。 +/// @warning 请勿再使用 self.selectedBackgroundView.backgroundColor 修改,因为 QMUITheme 里会重新应用 qmui_selectedBackgroundColor,会覆盖 self.selectedBackgroundView.backgroundColor 的效果。 +@property(nonatomic, strong, nullable) UIColor *qmui_selectedBackgroundColor; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UICollectionViewCell+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UICollectionViewCell+QMUI.m new file mode 100644 index 00000000..b6043a34 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UICollectionViewCell+QMUI.m @@ -0,0 +1,42 @@ +/** + * 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. + */ +// +// UICollectionViewCell+QMUI.m +// QMUIKit +// +// Created by MoLice on 2021/M/9. +// + +#import "UICollectionViewCell+QMUI.h" +#import "QMUICore.h" + +@interface UICollectionViewCell () +@property(nonatomic, strong) UIView *qmuicvc_selectedBackgroundView; +@end + +@implementation UICollectionViewCell (QMUI) + +QMUISynthesizeIdStrongProperty(qmuicvc_selectedBackgroundView, setQmuicvc_selectedBackgroundView) + +static char kAssociatedObjectKey_selectedBackgroundColor; +- (void)setQmui_selectedBackgroundColor:(UIColor *)qmui_selectedBackgroundColor { + objc_setAssociatedObject(self, &kAssociatedObjectKey_selectedBackgroundColor, qmui_selectedBackgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_selectedBackgroundColor && !self.selectedBackgroundView && !self.qmuicvc_selectedBackgroundView) { + self.qmuicvc_selectedBackgroundView = UIView.new; + self.selectedBackgroundView = self.qmuicvc_selectedBackgroundView; + } + if (self.qmuicvc_selectedBackgroundView) { + self.qmuicvc_selectedBackgroundView.backgroundColor = qmui_selectedBackgroundColor; + } +} + +- (UIColor *)qmui_selectedBackgroundColor { + return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_selectedBackgroundColor); +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIColor+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIColor+QMUI.h index ee1b8582..ffc3fa03 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIColor+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIColor+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIColor+QMUI.h @@ -17,12 +17,14 @@ #define UIColorMakeWithHex(hex) [UIColor qmui_colorWithHexString:hex] +NS_ASSUME_NONNULL_BEGIN + @interface UIColor (QMUI) /** * 使用HEX命名方式的颜色字符串生成一个UIColor对象 * - * @param hexString + * @param hexString 支持以 # 开头和不以 # 开头的 hex 字符串 * #RGB 例如#f0f,等同于#ffff00ff,RGBA(255, 0, 255, 1) * #ARGB 例如#0f0f,等同于#00ff00ff,RGBA(255, 0, 255, 0) * #RRGGBB 例如#ff00ff,等同于#ffff00ff,RGBA(255, 0, 255, 1) @@ -34,51 +36,63 @@ /** * 将当前色值转换为hex字符串,通道排序是AARRGGBB(与Android保持一致) + * @return 色值对应的 hex 字符串,以 # 开头,例如 #00ff00ff + */ +@property(nonatomic, copy, readonly) NSString *qmui_hexString; + +/** + 将一个 RGBA 字符串转换成 UIColor + @param rgbaString 支持 RGB 或者 RGBA,其中只有 alpha 支持小数点,取值范围为 [0.0-1.0],其他通道均为整数,取值范围 [0-255],通道之间用英文逗号或空格隔开,例如以下参数都是合法的:@"255,255,255,.1"、@"255,255,0"、@"255,255,255"、@"255 255 255" + */ ++ (nullable UIColor *)qmui_colorWithRGBAString:(nullable NSString *)rgbaString; + +/** + 将当前色值转换成“255,255,255,1.00”的字符串,如果 alpha 通道为1也会输出出来。其中 alpha 通道必定带两位小数点,其他三个通道都是整数。 */ -- (nonnull NSString *)qmui_hexString; +@property(nonatomic, copy, readonly) NSString *qmui_RGBAString; /** - * 获取当前UIColor对象里的红色色值 + * 获取当前 UIColor 对象里的红色色值 * * @return 红色通道的色值,值范围为0.0-1.0 */ -- (CGFloat)qmui_red; +@property(nonatomic, assign, readonly) CGFloat qmui_red; /** - * 获取当前UIColor对象里的绿色色值 + * 获取当前 UIColor 对象里的绿色色值 * * @return 绿色通道的色值,值范围为0.0-1.0 */ -- (CGFloat)qmui_green; +@property(nonatomic, assign, readonly) CGFloat qmui_green; /** - * 获取当前UIColor对象里的蓝色色值 + * 获取当前 UIColor 对象里的蓝色色值 * * @return 蓝色通道的色值,值范围为0.0-1.0 */ -- (CGFloat)qmui_blue; +@property(nonatomic, assign, readonly) CGFloat qmui_blue; /** - * 获取当前UIColor对象里的透明色值 + * 获取当前 UIColor 对象里的透明色值 * * @return 透明通道的色值,值范围为0.0-1.0 */ -- (CGFloat)qmui_alpha; +@property(nonatomic, assign, readonly) CGFloat qmui_alpha; /** - * 获取当前UIColor对象里的hue(色相) + * 获取当前 UIColor 对象里的 hue(色相),注意 hue 的值是一个角度,所以0和1(0°和360°)是等价的,用 return 值去做判断时要特别注意。 */ -- (CGFloat)qmui_hue; +@property(nonatomic, assign, readonly) CGFloat qmui_hue; /** - * 获取当前UIColor对象里的saturation(饱和度) + * 获取当前 UIColor 对象里的 saturation(饱和度) */ -- (CGFloat)qmui_saturation; +@property(nonatomic, assign, readonly) CGFloat qmui_saturation; /** - * 获取当前UIColor对象里的brightness(亮度) + * 获取当前 UIColor 对象里的 brightness(亮度) */ -- (CGFloat)qmui_brightness; +@property(nonatomic, assign, readonly) CGFloat qmui_brightness; /** * 将当前UIColor对象剥离掉alpha通道后得到的色值。相当于把当前颜色的半透明值强制设为1.0后返回 @@ -90,19 +104,19 @@ /** * 计算当前color叠加了alpha之后放在指定颜色的背景上的色值 */ -- (nonnull UIColor *)qmui_colorWithAlpha:(CGFloat)alpha backgroundColor:(nullable UIColor *)backgroundColor; +- (UIColor *)qmui_colorWithAlpha:(CGFloat)alpha backgroundColor:(nullable UIColor *)backgroundColor; /** * 计算当前color叠加了alpha之后放在白色背景上的色值 */ -- (nonnull UIColor *)qmui_colorWithAlphaAddedToWhite:(CGFloat)alpha; +- (UIColor *)qmui_colorWithAlphaAddedToWhite:(CGFloat)alpha; /** * 将自身变化到某个目标颜色,可通过参数progress控制变化的程度,最终得到一个纯色 * @param toColor 目标颜色 * @param progress 变化程度,取值范围0.0f~1.0f */ -- (nonnull UIColor *)qmui_transitionToColor:(nullable UIColor *)toColor progress:(CGFloat)progress; +- (UIColor *)qmui_transitionToColor:(nullable UIColor *)toColor progress:(CGFloat)progress; /** * 判断当前颜色是否为深色,可用于根据不同色调动态设置不同文字颜色的场景。 @@ -111,31 +125,37 @@ * * @return 若为深色则返回“YES”,浅色则返回“NO” */ -- (BOOL)qmui_colorIsDark; +@property(nonatomic, assign, readonly) BOOL qmui_colorIsDark; /** - * 当前颜色的反色 + * @return 当前颜色的反色,不管传入的颜色属于什么 colorSpace,最终返回的反色都是 RGB * * @link http://stackoverflow.com/questions/5893261/how-to-get-inverse-color-from-uicolor @/link */ -- (nonnull UIColor *)qmui_inverseColor; +- (UIColor *)qmui_inverseColor; /** * 判断当前颜色是否等于系统默认的 tintColor 颜色。 * 背景:如果将一个 UIView.tintColor 设置为 nil,表示这个 view 的 tintColor 希望跟随 superview.tintColor 变化而变化,所以设置完再获取 view.tintColor,得到的并非 nil,而是 superview.tintColor 的值,而如果整棵 view 层级树里的 view 都没有设置自己的 tintColor,则会返回系统默认的 tintColor(也即 [UIColor qmui_systemTintColor]),所以才提供这个方法用于代替判断 tintColor == nil 的作用。 */ -- (BOOL)qmui_isSystemTintColor; +@property(nonatomic, assign, readonly) BOOL qmui_isSystemTintColor; /** * 获取当前系统的默认 tintColor 色值 */ -+ (nonnull UIColor *)qmui_systemTintColor; +@property(class, nonatomic, strong, readonly) UIColor *qmui_systemTintColor; + +/** + 获取两个颜色之间的差异程度,0表示相同,值越大表示差距越大,例如纯白和纯黑会返回 86,如果遇到异常情况(例如传进来的 color 为 nil,则会返回 CGFLOAT_MAX)。 + 原理是将两个颜色摆放在 HSB(HSV) 模型内,取两个点之间的距离。由于 HSB(HSV) 没有 alpha 的概念,所以色值相同半透明程度不同的两个颜色会返回 0,也即相等。 + */ +- (CGFloat)qmui_distanceBetweenColor:(UIColor *)color; /** * 计算两个颜色叠加之后的最终色(注意区分前景色后景色的顺序)
* @link http://stackoverflow.com/questions/10781953/determine-rgba-colour-received-by-combining-two-colours @/link */ -+ (nonnull UIColor *)qmui_colorWithBackendColor:(nonnull UIColor *)backendColor frontColor:(nonnull UIColor *)frontColor; ++ (UIColor *)qmui_colorWithBackendColor:(UIColor *)backendColor frontColor:(UIColor *)frontColor; /** * 将颜色A变化到颜色B,可通过progress控制变化的程度 @@ -143,11 +163,43 @@ * @param toColor 目标颜色 * @param progress 变化程度,取值范围0.0f~1.0f */ -+ (nonnull UIColor *)qmui_colorFromColor:(nonnull UIColor *)fromColor toColor:(nonnull UIColor *)toColor progress:(CGFloat)progress; ++ (UIColor *)qmui_colorFromColor:(UIColor *)fromColor toColor:(UIColor *)toColor progress:(CGFloat)progress; /** * 产生一个随机色,大部分情况下用于测试 */ -+ (nonnull UIColor *)qmui_randomColor; ++ (UIColor *)qmui_randomColor; @end + + +/// 将原本的 dynamic color 绑定到 CGColorRef 上的 key +extern NSString *const QMUICGColorOriginalColorBindKey; + +@protocol QMUIDynamicColorProtocol + +@required + +/// 获取当前 color 的标记名称,仅对 QMUIThemeColor 有效,其他 class 返回 nil。 +@property(nonatomic, copy, readonly) NSString *qmui_name; + +/// 获取当前 color 的实际颜色(返回的颜色必定不是 dynamic color) +@property(nonatomic, strong, readonly) UIColor *qmui_rawColor; + +/// 标志当前 UIColor 对象是否为动态颜色(由 [UIColor qmui_colorWithThemeProvider:] 创建的颜色,或者 iOS 13 下由 [UIColor colorWithDynamicProvider:]、[UIColor initWithDynamicProvider:] 创建的颜色) +@property(nonatomic, assign, readonly) BOOL qmui_isDynamicColor; + +/// 标志当前 UIColor 对象是否为 QMUIThemeColor +@property(nonatomic, assign, readonly) BOOL qmui_isQMUIDynamicColor; + +@optional +/// 这方法其实是 iOS 13 新增的 UIDynamicColor 里的私有方法,只要任意 UIColor 的类实现这个方法并返回 YES,就能自动响应 iOS 13 下的 UIUserInterfaceStyle 的切换,这里在 protocol 里声明是为了方便 .m 里调用(否则会因为不存在的 selector 而无法编译) +@property(nonatomic, assign, readonly) BOOL _isDynamic; + +@end + +@interface UIColor (QMUI_DynamicColor) + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIColor+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIColor+QMUI.m index 478775c3..af1140cd 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIColor+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIColor+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIColor+QMUI.m @@ -16,6 +16,8 @@ #import "UIColor+QMUI.h" #import "QMUICore.h" #import "NSString+QMUI.h" +#import "NSObject+QMUI.h" +#import "NSArray+QMUI.h" @implementation UIColor (QMUI) @@ -65,7 +67,7 @@ + (UIColor *)qmui_colorWithHexString:(NSString *)hexString { blue = [self colorComponentFrom: colorString start: 6 length: 2]; break; default: { - NSAssert(NO, @"Color value %@ is invalid. It should be a hex value of the form #RBG, #ARGB, #RRGGBB, or #AARRGGBB", hexString); + QMUIAssert(NO, @"UIColor (QMUI)", @"Color value %@ is invalid. It should be a hex value of the form #RBG, #ARGB, #RRGGBB, or #AARRGGBB", hexString); return nil; } break; @@ -85,6 +87,29 @@ - (NSString *)qmui_hexString { [self alignColorHexStringLength:[NSString qmui_hexStringWithInteger:blue]]] lowercaseString]; } ++ (UIColor *)qmui_colorWithRGBAString:(NSString *)rgbaString { + NSArray *arr = nil; + NSCharacterSet *characterSet = nil; + if ([rgbaString containsString:@","]) { + characterSet = [NSCharacterSet characterSetWithCharactersInString:@","]; + } else { + characterSet = [NSCharacterSet characterSetWithCharactersInString:@" "]; + } + arr = [[rgbaString componentsSeparatedByCharactersInSet:characterSet] qmui_filterWithBlock:^BOOL(NSString * _Nonnull item) { + return item.qmui_trim.length > 0; + }]; + if (arr.count < 3 || arr.count > 4) return nil; + return UIColorMakeWithRGBA(arr[0].integerValue, arr[1].integerValue, arr[2].integerValue, (arr.count == 4 ? arr[3].floatValue : 1.0)); +} + +- (NSString *)qmui_RGBAString { + return [NSString stringWithFormat:@"%.0f,%.0f,%.0f,%.2f", + round(self.qmui_red * 255), + round(self.qmui_green * 255), + round(self.qmui_blue * 255), + self.qmui_alpha]; +} + // 对于色值只有单位数的,在前面补一个0,例如“F”会补齐为“0F” - (NSString *)alignColorHexStringLength:(NSString *)hexString { return hexString.length < 2 ? [@"0" stringByAppendingString:hexString] : hexString; @@ -179,8 +204,8 @@ - (UIColor *)qmui_transitionToColor:(UIColor *)toColor progress:(CGFloat)progres } - (BOOL)qmui_colorIsDark { - CGFloat red = 0.0, green = 0.0, blue = 0.0, alpha = 0.0; - if ([self getRed:&red green:&green blue:&blue alpha:&alpha]) { + CGFloat red = 0.0, green = 0.0, blue = 0.0; + if ([self getRed:&red green:&green blue:&blue alpha:0]) { float referenceValue = 0.411; float colorDelta = ((red * 0.299) + (green * 0.587) + (blue * 0.114)); @@ -202,6 +227,35 @@ - (BOOL)qmui_isSystemTintColor { return [self isEqual:[UIColor qmui_systemTintColor]]; } +- (CGFloat)qmui_distanceBetweenColor:(UIColor *)color { + if (!color) return CGFLOAT_MAX; + + UIColor *color1 = self; + UIColor *color2 = color; + CGFloat R = 100.0; + CGFloat angle = 30.0; + CGFloat h = R * cos(angle / 180 * M_PI); + CGFloat r = R * sin(angle / 180 * M_PI); + + CGFloat hue1 = color1.qmui_hue * 360; + CGFloat saturation1 = color1.qmui_saturation; + CGFloat brightness1 = color1.qmui_brightness; + CGFloat hue2 = color2.qmui_hue * 360; + CGFloat saturation2 = color2.qmui_saturation; + CGFloat brightness2 = color2.qmui_brightness; + + CGFloat x1 = r * brightness1 * saturation1 * cos(hue1 / 180 * M_PI); + CGFloat y1 = r * brightness1 * saturation1 * sin(hue1 / 180 * M_PI); + CGFloat z1 = h * (1 - brightness1); + CGFloat x2 = r * brightness2 * saturation2 * cos(hue2 / 180 * M_PI); + CGFloat y2 = r * brightness2 * saturation2 * sin(hue2 / 180 * M_PI); + CGFloat z2 = h * (1 - brightness2); + CGFloat dx = x1 - x2; + CGFloat dy = y1 - y2; + CGFloat dz = z1 - z2; + return sqrt(dx * dx + dy * dy + dz * dz); +} + + (UIColor *)qmui_systemTintColor { static UIColor *systemTintColor = nil; if (!systemTintColor) { @@ -257,3 +311,67 @@ + (UIColor *)qmui_randomColor { } @end + + +NSString *const QMUICGColorOriginalColorBindKey = @"QMUICGColorOriginalColorBindKey"; + +@implementation UIColor (QMUI_DynamicColor) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation([UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull trait) { + return [UIColor clearColor]; + }].class, @selector(CGColor), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGColorRef(UIColor *selfObject) { + // call super + CGColorRef (*originSelectorIMP)(id, SEL); + originSelectorIMP = (CGColorRef (*)(id, SEL))originalIMPProvider(); + CGColorRef result = originSelectorIMP(selfObject, originCMD); + + if (selfObject.qmui_isDynamicColor) { + + // copy + UIColor *color = [UIColor colorWithCGColor:result]; + + // CGColor 必须通过 CGColorCreate 创建。UIColor.CGColor 返回的是一个多对象复用的 CGColor 值(例如,如果 QMUIThemeA.light 值和 UIColorB 的值刚好相同,那么他们的 CGColor 可能也是同一个对象,所以 UIColorB.CGColor 可能会错误地使用了原本仅属于 QMUIThemeColorA 的 bindObject) + // 经测试,qmui_red 系列接口适用于不同的 ColorSpace,应该是能放心使用的😜 + // https://github.com/Tencent/QMUI_iOS/issues/1463 + CGColorSpaceRef spaceRef = CGColorSpaceCreateDeviceRGB(); + result = CGColorCreate(spaceRef, (CGFloat[]){color.qmui_red, color.qmui_green, color.qmui_blue, color.qmui_alpha}); + CGColorSpaceRelease(spaceRef); + + [(__bridge id)(result) qmui_bindObject:selfObject forKey:QMUICGColorOriginalColorBindKey]; + return (CGColorRef)CFAutorelease(result); + } + + return result; + }; + }); + }); +} + +- (BOOL)qmui_isDynamicColor { + if ([self respondsToSelector:@selector(_isDynamic)]) { + return self._isDynamic; + } + return NO; +} + +- (BOOL)qmui_isQMUIDynamicColor { + return NO; +} + +- (NSString *)qmui_name { + return nil; +} + +- (UIColor *)qmui_rawColor { + if (self.qmui_isDynamicColor && [self respondsToSelector:@selector(resolvedColorWithTraitCollection:)]) { + UIColor *color = [self resolvedColorWithTraitCollection:UITraitCollection.currentTraitCollection]; + return color.qmui_rawColor; + } + return self; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIControl+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIControl+QMUI.h index 2a42c20a..4f40d142 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIControl+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIControl+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIControl+QMUI.h @@ -18,22 +18,32 @@ @interface UIControl (QMUI) /** - * 是否接管 UIControl 的 touch 事件。 - * - * UIControl 在 UIScrollView 上会有300毫秒的延迟,默认情况下快速点击某个 UIControl,将不会看到 setHighlighted 的效果。如果通过将 UIScrollView.delaysContentTouches 置为 NO 来取消这个延迟,则系统无法判断 touch 时是要点击还是要滚动。 + * 是否优化 UIControl 被放在 UIScrollView 上时的点击体验。系统默认行为下,UIControl 在 UIScrollView 上会有300毫秒的延迟,当你快速点击某个 UIControl 时,将不会看到 setHighlighted 的效果。 * * 此时可以将 UIControl.qmui_automaticallyAdjustTouchHighlightedInScrollView 属性置为 YES,会使用自己的一套计算方式去判断触发 setHighlighted 的时机,从而保证既不影响 UIScrollView 的滚动,又能让 UIControl 在被快速点击时也能立马看到 setHighlighted 的效果。 * - * @warning 使用了这个属性则不需要设置 UIScrollView.delaysContentTouches。 + * @warning 使用了这个属性则不需要设置 UIScrollView.delaysContentTouches。因为如果将 UIScrollView.delaysContentTouches 置为 NO 来取消这个延迟,则系统无法判断 touch 时是要点击还是要滚动,你就会观察到当你想要滚动 UIScrollView 时,手指触摸到的那个 UIControl 会呈现出 highlighted 的效果,但通常这并不符合预期。 */ @property(nonatomic, assign) BOOL qmui_automaticallyAdjustTouchHighlightedInScrollView; -/// 响应区域需要改变的大小,负值表示往外扩大,正值表示往内缩小 -@property(nonatomic,assign) UIEdgeInsets qmui_outsideEdge; +/** + 当快速重复点击某个 UIControl 时,系统的默认行为是每次点击都会触发一次 UIControlEventTouchUpInside 事件,但通常这并不是我们想要的,可能会导致某段逻辑被重复触发。因此提供这个属性,当置为 YES 时,连续的快速点击只有第一次会触发 UIControlEventTouchUpInside,当停止300ms后再重新点击,才会重新触发一次 UIControlEventTouchUpInside。该属性对非 UIControlEventTouchUpInside 的事件无效(例如 UIControlEventTouchDownRepeat、UIControlEventEditingChanged 等事件本来就会短时间内重复被触发多次)。 + + @note 系统默认就会对同一点击区域短时间内触发的多次 touch 都归到同一组,所以如果10s内连续不断地快速点击同一个按钮,这10s的时间里也只会触发一次 UIControlEventTouchUpInside,因为这10s里的所有 touch 都被归到同一组事件里。但如果通过定时器实现,假设以1s为临界点,那么这10s的快速点击就会触发十次。QMUI 的实现采用的是前一种。 + + @warning 不能与 @c qmui_automaticallyAdjustTouchHighlightedInScrollView 同时开启。 + */ +@property(nonatomic, assign) BOOL qmui_preventsRepeatedTouchUpInsideEvent; /// setHighlighted: 方法的回调 block @property(nonatomic, copy) void (^qmui_setHighlightedBlock)(BOOL highlighted); +/// setSelected: 方法的回调 block +@property(nonatomic, copy) void (^qmui_setSelectedBlock)(BOOL selected); + +/// setEnabled: 方法的回调 block +@property(nonatomic, copy) void (^qmui_setEnabledBlock)(BOOL enabled); + /// 等同于 addTarget:action:forControlEvents:UIControlEventTouchUpInside @property(nonatomic, copy) void (^qmui_tapBlock)(__kindof UIControl *sender); diff --git a/QMUI/QMUIKit/UIKitExtensions/UIControl+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIControl+QMUI.m index 599446e0..130c165f 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIControl+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIControl+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIControl+QMUI.m @@ -14,136 +14,80 @@ // #import "UIControl+QMUI.h" -#import #import "QMUICore.h" @interface UIControl () -@property(nonatomic,assign) BOOL canSetHighlighted; -@property(nonatomic,assign) NSInteger touchEndCount; - +@property(nonatomic,assign) BOOL qmuictl_canSetHighlighted; +@property(nonatomic,assign) NSInteger qmuictl_touchEndCount; @end @implementation UIControl (QMUI) -QMUISynthesizeUIEdgeInsetsProperty(qmui_outsideEdge, setQmui_outsideEdge) -QMUISynthesizeBOOLProperty(qmui_automaticallyAdjustTouchHighlightedInScrollView, setQmui_automaticallyAdjustTouchHighlightedInScrollView) -QMUISynthesizeBOOLProperty(canSetHighlighted, setCanSetHighlighted) -QMUISynthesizeNSIntegerProperty(touchEndCount, setTouchEndCount) -QMUISynthesizeIdCopyProperty(qmui_setHighlightedBlock, setQmui_setHighlightedBlock) - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - - ExtendImplementationOfVoidMethodWithSingleArgument([UIControl class], @selector(setHighlighted:), BOOL, ^(UIControl *selfObject, BOOL highlighted) { - if (selfObject.qmui_setHighlightedBlock) { - selfObject.qmui_setHighlightedBlock(highlighted); - } - }); - - OverrideImplementation([UIControl class], @selector(pointInside:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^BOOL(UIControl *selfObject, CGPoint point, UIEvent *event) { - - // avoid superclass - if ((![selfObject isKindOfClass:originClass]) || (event.type != UIEventTypeTouches)) { +QMUISynthesizeBOOLProperty(qmuictl_canSetHighlighted, setQmuictl_canSetHighlighted) +QMUISynthesizeNSIntegerProperty(qmuictl_touchEndCount, setQmuictl_touchEndCount) + +#pragma mark - Automatically Adjust Touch Highlighted In ScrollView + +static char kAssociatedObjectKey_automaticallyAdjustTouchHighlightedInScrollView; +- (void)setQmui_automaticallyAdjustTouchHighlightedInScrollView:(BOOL)qmui_automaticallyAdjustTouchHighlightedInScrollView { + objc_setAssociatedObject(self, &kAssociatedObjectKey_automaticallyAdjustTouchHighlightedInScrollView, @(qmui_automaticallyAdjustTouchHighlightedInScrollView), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_automaticallyAdjustTouchHighlightedInScrollView) { + [QMUIHelper executeBlock:^{ + OverrideImplementation([UIControl class], @selector(touchesBegan:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIControl *selfObject, NSSet *touches, UIEvent *event) { + + // call super + void (^callSuperBlock)(void) = ^{ + void (*originSelectorIMP)(id, SEL, NSSet *, UIEvent *); + originSelectorIMP = (void (*)(id, SEL, NSSet *, UIEvent *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, touches, event); + }; + + selfObject.qmuictl_touchEndCount = 0; + if (selfObject.qmui_automaticallyAdjustTouchHighlightedInScrollView) { + selfObject.qmuictl_canSetHighlighted = YES; + callSuperBlock(); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (selfObject.qmuictl_canSetHighlighted) { + [selfObject setHighlighted:YES]; + } + }); + } else { + callSuperBlock(); + } + }; + }); + + OverrideImplementation([UIControl class], @selector(touchesMoved:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIControl *selfObject, NSSet *touches, UIEvent *event) { + + if (selfObject.qmui_automaticallyAdjustTouchHighlightedInScrollView) { + selfObject.qmuictl_canSetHighlighted = NO; + } + // call super - BOOL (*originSelectorIMP)(id, SEL, CGPoint, UIEvent *); - originSelectorIMP = (BOOL (*)(id, SEL, CGPoint, UIEvent *))originalIMPProvider(); - BOOL result = originSelectorIMP(selfObject, originCMD, point, event); - return result; - } - - UIEdgeInsets qmui_outsideEdge = selfObject.qmui_outsideEdge; - CGRect boundsInsetOutsideEdge = CGRectMake(CGRectGetMinX(selfObject.bounds) + qmui_outsideEdge.left, CGRectGetMinY(selfObject.bounds) + qmui_outsideEdge.top, CGRectGetWidth(selfObject.bounds) - UIEdgeInsetsGetHorizontalValue(qmui_outsideEdge), CGRectGetHeight(selfObject.bounds) - UIEdgeInsetsGetVerticalValue(qmui_outsideEdge)); - return CGRectContainsPoint(boundsInsetOutsideEdge, point); - }; - }); - - OverrideImplementation([UIControl class], @selector(removeTarget:action:forControlEvents:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIControl *selfObject, id target, SEL action, UIControlEvents controlEvents) { - - // call super - void (*originSelectorIMP)(id, SEL, id, SEL, UIControlEvents); - originSelectorIMP = (void (*)(id, SEL, id, SEL, UIControlEvents))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, target, action, controlEvents); - - // avoid superclass - if (![selfObject isKindOfClass:originClass]) return; - - BOOL isTouchUpInsideEvent = controlEvents & UIControlEventTouchUpInside; - BOOL shouldRemoveTouchUpInsideSelector = (action == @selector(qmui_handleTouchUpInside:)) || (target == selfObject && !action) || (!target && !action); - if (isTouchUpInsideEvent && shouldRemoveTouchUpInsideSelector) { - // 避免触发 setter 又反过来 removeTarget,然后就死循环了 - objc_setAssociatedObject(selfObject, &kAssociatedObjectKey_tapBlock, nil, OBJC_ASSOCIATION_COPY_NONATOMIC); - } - }; - }); - - OverrideImplementation([UIControl class], @selector(touchesBegan:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIControl *selfObject, NSSet *touches, UIEvent *event) { - - // call super - void (^callSuperBlock)(void) = ^{ void (*originSelectorIMP)(id, SEL, NSSet *, UIEvent *); originSelectorIMP = (void (*)(id, SEL, NSSet *, UIEvent *))originalIMPProvider(); originSelectorIMP(selfObject, originCMD, touches, event); }; - - // avoid superclass - if (![selfObject isKindOfClass:originClass]) { - callSuperBlock(); - return; - } - - selfObject.touchEndCount = 0; - if (selfObject.qmui_automaticallyAdjustTouchHighlightedInScrollView) { - selfObject.canSetHighlighted = YES; - callSuperBlock(); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - if (selfObject.canSetHighlighted) { - [selfObject setHighlighted:YES]; - } - }); - } else { - callSuperBlock(); - } - }; - }); - - OverrideImplementation([UIControl class], @selector(touchesMoved:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIControl *selfObject, NSSet *touches, UIEvent *event) { - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (selfObject.qmui_automaticallyAdjustTouchHighlightedInScrollView) { - selfObject.canSetHighlighted = NO; - } - } - - // call super - void (*originSelectorIMP)(id, SEL, NSSet *, UIEvent *); - originSelectorIMP = (void (*)(id, SEL, NSSet *, UIEvent *))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, touches, event); - }; - }); - - OverrideImplementation([UIControl class], @selector(touchesEnded:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIControl *selfObject, NSSet *touches, UIEvent *event) { - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { + }); + + OverrideImplementation([UIControl class], @selector(touchesEnded:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIControl *selfObject, NSSet *touches, UIEvent *event) { + if (selfObject.qmui_automaticallyAdjustTouchHighlightedInScrollView) { - selfObject.canSetHighlighted = NO; + selfObject.qmuictl_canSetHighlighted = NO; if (selfObject.touchInside) { [selfObject setHighlighted:YES]; + __weak __typeof(selfObject)weakSelf = selfObject;// 避免 dispatch retain 住 self,因为这期间可能 self 已经被 remove 了,如果还触发它的点击事件,可能导致业务逻辑异常 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.02 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 如果延迟时间太长,会导致快速点击两次,事件会触发两次 // 对于 3D Touch 的机器,如果点击按钮的时候在按钮上停留事件稍微长一点点,那么 touchesEnded 会被调用两次 // 把 super touchEnded 放到延迟里调用会导致长按无法触发点击,先这么改,再想想怎么办。// [selfObject qmui_touchesEnded:touches withEvent:event]; - [selfObject sendActionsForAllTouchEventsIfCan]; - if (selfObject.highlighted) { - [selfObject setHighlighted:NO]; + [weakSelf sendActionsForAllTouchEventsIfCan]; + if (weakSelf.highlighted) { + [weakSelf setHighlighted:NO]; } }); } else { @@ -151,55 +95,200 @@ + (void)load { } return; } - } - - // call super - void (*originSelectorIMP)(id, SEL, NSSet *, UIEvent *); - originSelectorIMP = (void (*)(id, SEL, NSSet *, UIEvent *))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, touches, event); - }; - }); - - OverrideImplementation([UIControl class], @selector(touchesCancelled:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIControl *selfObject, NSSet *touches, UIEvent *event) { - - // call super - void (^callSuperBlock)(void) = ^{ + + // call super void (*originSelectorIMP)(id, SEL, NSSet *, UIEvent *); originSelectorIMP = (void (*)(id, SEL, NSSet *, UIEvent *))originalIMPProvider(); originSelectorIMP(selfObject, originCMD, touches, event); }; - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { + }); + + OverrideImplementation([UIControl class], @selector(touchesCancelled:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIControl *selfObject, NSSet *touches, UIEvent *event) { + + // call super + void (^callSuperBlock)(void) = ^{ + void (*originSelectorIMP)(id, SEL, NSSet *, UIEvent *); + originSelectorIMP = (void (*)(id, SEL, NSSet *, UIEvent *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, touches, event); + }; + if (selfObject.qmui_automaticallyAdjustTouchHighlightedInScrollView) { - selfObject.canSetHighlighted = NO; + selfObject.qmuictl_canSetHighlighted = NO; callSuperBlock(); if (selfObject.highlighted) { [selfObject setHighlighted:NO]; } return; } - } - callSuperBlock(); - }; - }); - }); + callSuperBlock(); + }; + }); + } oncePerIdentifier:@"UIControl automaticallyAdjustTouchHighlightedInScrollView"]; + } +} + +- (BOOL)qmui_automaticallyAdjustTouchHighlightedInScrollView { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_automaticallyAdjustTouchHighlightedInScrollView)) boolValue]; } // 这段代码需要以一个独立的方法存在,因为一旦有坑,外面可以直接通过runtime调用这个方法 // 但,不要开放到.h文件里,理论上外面不应该用到它 - (void)sendActionsForAllTouchEventsIfCan { - self.touchEndCount += 1; - if (self.touchEndCount == 1) { + self.qmuictl_touchEndCount += 1; + if (self.qmuictl_touchEndCount == 1) { [self sendActionsForControlEvents:UIControlEventAllTouchEvents]; } } +#pragma mark - Prevents Repeated TouchUpInside Event + +static char kAssociatedObjectKey_preventsRepeatedTouchUpInsideEvent; +- (void)setQmui_preventsRepeatedTouchUpInsideEvent:(BOOL)qmui_preventsRepeatedTouchUpInsideEvent { + objc_setAssociatedObject(self, &kAssociatedObjectKey_preventsRepeatedTouchUpInsideEvent, @(qmui_preventsRepeatedTouchUpInsideEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_preventsRepeatedTouchUpInsideEvent) { + [QMUIHelper executeBlock:^{ + + OverrideImplementation([UIControl class], @selector(sendAction:to:forEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIControl *selfObject, SEL action, id target, UIEvent *event) { + + if (selfObject.qmui_preventsRepeatedTouchUpInsideEvent) { + NSArray *actions = [selfObject actionsForTarget:target forControlEvent:UIControlEventTouchUpInside]; + if (!actions) { + // iOS 10 UIBarButtonItem 里的 UINavigationButton 点击事件用的是 UIControlEventPrimaryActionTriggered + actions = [selfObject actionsForTarget:target forControlEvent:UIControlEventPrimaryActionTriggered]; + } + if ([actions containsObject:NSStringFromSelector(action)]) { + UITouch *touch = event.allTouches.anyObject; + if (touch.tapCount > 1) { + return; + } + } + } + + // call super + void (*originSelectorIMP)(id, SEL, SEL, id, UIEvent *); + originSelectorIMP = (void (*)(id, SEL, SEL, id, UIEvent *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, action, target, event); + }; + }); + } oncePerIdentifier:@"UIControl preventsRepeatedTouchUpInsideEvent"]; + } +} + +- (BOOL)qmui_preventsRepeatedTouchUpInsideEvent { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_preventsRepeatedTouchUpInsideEvent)) boolValue]; +} + +#pragma mark - Highlighted Block + +static char kAssociatedObjectKey_setHighlightedBlock; +- (void)setQmui_setHighlightedBlock:(void (^)(BOOL))qmui_setHighlightedBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_setHighlightedBlock, qmui_setHighlightedBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_setHighlightedBlock) { + [QMUIHelper executeBlock:^{ + OverrideImplementation([UIControl class], @selector(setHighlighted:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIControl *selfObject, BOOL highlighted) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, highlighted); + + if (selfObject.qmui_setHighlightedBlock) { + selfObject.qmui_setHighlightedBlock(highlighted); + } + }; + }); + } oncePerIdentifier:@"UIControl setHighlighted:"]; + } +} + +- (void (^)(BOOL))qmui_setHighlightedBlock { + return (void (^)(BOOL))objc_getAssociatedObject(self, &kAssociatedObjectKey_setHighlightedBlock); +} + +#pragma mark - Selected Block + +static char kAssociatedObjectKey_setSelectedBlock; +- (void)setQmui_setSelectedBlock:(void (^)(BOOL))qmui_setSelectedBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_setSelectedBlock, qmui_setSelectedBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_setSelectedBlock) { + [QMUIHelper executeBlock:^{ + OverrideImplementation([UIControl class], @selector(setSelected:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIControl *selfObject, BOOL selected) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, selected); + + if (selfObject.qmui_setSelectedBlock) { + selfObject.qmui_setSelectedBlock(selected); + } + }; + }); + } oncePerIdentifier:@"UIControl setSelected:"]; + } +} + +- (void (^)(BOOL))qmui_setSelectedBlock { + return (void (^)(BOOL))objc_getAssociatedObject(self, &kAssociatedObjectKey_setSelectedBlock); +} + +#pragma mark - Enabled Block + +static char kAssociatedObjectKey_setEnabledBlock; +- (void)setQmui_setEnabledBlock:(void (^)(BOOL))qmui_setEnabledBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_setEnabledBlock, qmui_setEnabledBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_setEnabledBlock) { + [QMUIHelper executeBlock:^{ + OverrideImplementation([UIControl class], @selector(setEnabled:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIControl *selfObject, BOOL enabled) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, enabled); + + if (selfObject.qmui_setEnabledBlock) { + selfObject.qmui_setEnabledBlock(enabled); + } + }; + }); + } oncePerIdentifier:@"UIControl setEnabled:"]; + } +} + +- (void (^)(BOOL))qmui_setEnabledBlock { + return (void (^)(BOOL))objc_getAssociatedObject(self, &kAssociatedObjectKey_setEnabledBlock); +} + #pragma mark - Tap Block static char kAssociatedObjectKey_tapBlock; - (void)setQmui_tapBlock:(void (^)(__kindof UIControl *))qmui_tapBlock { + if (qmui_tapBlock) { + [QMUIHelper executeBlock:^{ + OverrideImplementation([UIControl class], @selector(removeTarget:action:forControlEvents:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIControl *selfObject, id target, SEL action, UIControlEvents controlEvents) { + + // call super + void (*originSelectorIMP)(id, SEL, id, SEL, UIControlEvents); + originSelectorIMP = (void (*)(id, SEL, id, SEL, UIControlEvents))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, target, action, controlEvents); + + BOOL isTouchUpInsideEvent = controlEvents & UIControlEventTouchUpInside; + BOOL shouldRemoveTouchUpInsideSelector = (action == @selector(qmui_handleTouchUpInside:)) || (target == selfObject && !action) || (!target && !action); + if (isTouchUpInsideEvent && shouldRemoveTouchUpInsideSelector) { + // 避免触发 setter 又反过来 removeTarget,然后就死循环了 + objc_setAssociatedObject(selfObject, &kAssociatedObjectKey_tapBlock, nil, OBJC_ASSOCIATION_COPY_NONATOMIC); + } + }; + }); + } oncePerIdentifier:@"UIControl tapBlock"]; + } + SEL action = @selector(qmui_handleTouchUpInside:); if (!qmui_tapBlock) { [self removeTarget:self action:action forControlEvents:UIControlEventTouchUpInside]; diff --git a/QMUI/QMUIKit/UIKitExtensions/UIFont+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIFont+QMUI.h index e5950ac0..25a587e2 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIFont+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIFont+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIFont+QMUI.h @@ -17,17 +17,27 @@ #define UIFontLightMake(size) [UIFont qmui_lightSystemFontOfSize:size] #define UIFontLightWithFont(_font) [UIFont qmui_lightSystemFontOfSize:_font.pointSize] + +#define UIFontMediumMake(size) [UIFont qmui_mediumSystemFontOfSize:size] +#define UIFontMediumWithFont(_font) [UIFont qmui_mediumSystemFontOfSize:_font.pointSize] + #define UIDynamicFontMake(_pointSize) [UIFont qmui_dynamicSystemFontOfSize:_pointSize weight:QMUIFontWeightNormal italic:NO] #define UIDynamicFontMakeWithLimit(_pointSize, _upperLimitSize, _lowerLimitSize) [UIFont qmui_dynamicSystemFontOfSize:_pointSize upperLimitSize:_upperLimitSize lowerLimitSize:_lowerLimitSize weight:QMUIFontWeightNormal italic:NO] -#define UIDynamicFontBoldMake(_pointSize) [UIFont qmui_dynamicSystemFontOfSize:_pointSize weight:QMUIFontWeightBold italic:NO] -#define UIDynamicFontBoldMakeWithLimit(_pointSize, _upperLimitSize, _lowerLimitSize) [UIFont qmui_dynamicSystemFontOfSize:_pointSize upperLimitSize:_upperLimitSize lowerLimitSize:_lowerLimitSize weight:QMUIFontWeightBold italic:NO] + #define UIDynamicFontLightMake(_pointSize) [UIFont qmui_dynamicSystemFontOfSize:_pointSize weight:QMUIFontWeightLight italic:NO] #define UIDynamicFontLightMakeWithLimit(_pointSize, _upperLimitSize, _lowerLimitSize) [UIFont qmui_dynamicSystemFontOfSize:_pointSize upperLimitSize:_upperLimitSize lowerLimitSize:_lowerLimitSize weight:QMUIFontWeightLight italic:NO] +#define UIDynamicFontMediumMake(_pointSize) [UIFont qmui_dynamicSystemFontOfSize:_pointSize weight:QMUIFontWeightMedium italic:NO] +#define UIDynamicFontMediumMakeWithLimit(_pointSize, _upperLimitSize, _lowerLimitSize) [UIFont qmui_dynamicSystemFontOfSize:_pointSize upperLimitSize:_upperLimitSize lowerLimitSize:_lowerLimitSize weight:QMUIFontWeightMedium italic:NO] + +#define UIDynamicFontBoldMake(_pointSize) [UIFont qmui_dynamicSystemFontOfSize:_pointSize weight:QMUIFontWeightBold italic:NO] +#define UIDynamicFontBoldMakeWithLimit(_pointSize, _upperLimitSize, _lowerLimitSize) [UIFont qmui_dynamicSystemFontOfSize:_pointSize upperLimitSize:_upperLimitSize lowerLimitSize:_lowerLimitSize weight:QMUIFontWeightBold italic:NO] + typedef NS_ENUM(NSUInteger, QMUIFontWeight) { - QMUIFontWeightLight, - QMUIFontWeightNormal, - QMUIFontWeightBold + QMUIFontWeightLight, // 对应 UIFontWeightLight + QMUIFontWeightNormal, // 对应 UIFontWeightRegular + QMUIFontWeightMedium, // 对应 UIFontWeightMedium + QMUIFontWeightBold // 对应 UIFontWeightSemibold }; @interface UIFont (QMUI) @@ -38,9 +48,20 @@ typedef NS_ENUM(NSUInteger, QMUIFontWeight) { * @param fontSize 字体大小 * * @return 变细的系统字体的 UIFont 对象 + * @see UIFontLightMake */ + (UIFont *)qmui_lightSystemFontOfSize:(CGFloat)fontSize; +/** + * 返回系统 Medium 字重的字体 + * + * @param fontSize 字体大小 + * + * @return Medium 系统字体的 UIFont 对象 + * @see UIFontMediumMake + */ ++ (UIFont *)qmui_mediumSystemFontOfSize:(CGFloat)fontSize; + /** * 根据需要生成一个 UIFont 对象并返回 * @param size 字号大小 diff --git a/QMUI/QMUIKit/UIKitExtensions/UIFont+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIFont+QMUI.m index 849acdef..3c4f0b72 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIFont+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIFont+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIFont+QMUI.m @@ -19,46 +19,42 @@ @implementation UIFont (QMUI) + (UIFont *)qmui_lightSystemFontOfSize:(CGFloat)fontSize { - return [UIFont fontWithName:@".SFUIText-Light" size:fontSize]; + return [UIFont systemFontOfSize:fontSize weight:UIFontWeightLight]; +} + ++ (UIFont *)qmui_mediumSystemFontOfSize:(CGFloat)fontSize { + return [UIFont systemFontOfSize:fontSize weight:UIFontWeightMedium]; } + (UIFont *)qmui_systemFontOfSize:(CGFloat)size weight:(QMUIFontWeight)weight italic:(BOOL)italic { - BOOL isLight = weight == QMUIFontWeightLight; - BOOL isBold = weight == QMUIFontWeightBold; - - BOOL shouldUsingHardCode = IOS_VERSION < 10.0;// 这 UIFontDescriptor 也是醉人,相同代码只有 iOS 10 能得出正确结果,7-9都无法获取到 Light + Italic 的字体,只能写死。 - if (shouldUsingHardCode) { - NSString *name = @".SFUIText"; - NSString *fontSuffix = [NSString stringWithFormat:@"%@%@", isLight ? @"Light" : (isBold ? @"Bold" : @""), italic ? @"Italic" : @""]; - NSString *fontName = [NSString stringWithFormat:@"%@%@%@", name, fontSuffix.length > 0 ? @"-" : @"", fontSuffix]; - UIFont *font = [UIFont fontWithName:fontName size:size]; - return font; - } - - // iOS 10 以上使用常规写法 UIFont *font = nil; - if (@available(iOS 8.2, *)) { - font = [UIFont systemFontOfSize:size weight:isLight ? UIFontWeightLight : (isBold ? UIFontWeightBold : UIFontWeightRegular)]; - - // 后面那些都是对斜体的操作,所以如果不需要斜体就直接 return - if (!italic) { - return font; + UIFontWeight fontWeight = ({ + UIFontWeight w; + switch (weight) { + case QMUIFontWeightLight: + w = UIFontWeightLight; + break; + case QMUIFontWeightMedium: + w = UIFontWeightMedium; + break; + case QMUIFontWeightBold: + w = UIFontWeightSemibold; + break; + default: + w = UIFontWeightRegular; + break; } - } else { - font = [UIFont systemFontOfSize:size]; + w; + }); + font = [UIFont systemFontOfSize:size weight:fontWeight]; + if (!italic) { + return font; } UIFontDescriptor *fontDescriptor = font.fontDescriptor; - NSMutableDictionary *traitsAttribute = [NSMutableDictionary dictionaryWithDictionary:fontDescriptor.fontAttributes[UIFontDescriptorTraitsAttribute]]; - if (![UIFont respondsToSelector:@selector(systemFontOfSize:weight:)]) { - traitsAttribute[UIFontWeightTrait] = isLight ? @-1.0 : (isBold ? @1.0 : @0.0); - } - if (italic) { - traitsAttribute[UIFontSlantTrait] = @1.0; - } else { - traitsAttribute[UIFontSlantTrait] = @0.0; - } - fontDescriptor = [fontDescriptor fontDescriptorByAddingAttributes:@{UIFontDescriptorTraitsAttribute: traitsAttribute}]; + UIFontDescriptorSymbolicTraits trait = fontDescriptor.symbolicTraits; + trait |= UIFontDescriptorTraitItalic; + fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:trait]; font = [UIFont fontWithDescriptor:fontDescriptor size:0]; return font; } diff --git a/QMUI/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.h index fc4746ed..2fb029fc 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIGestureRecognizer+QMUI.h diff --git a/QMUI/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.m index 486320b8..203eee65 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIGestureRecognizer+QMUI.m @@ -14,9 +14,41 @@ // #import "UIGestureRecognizer+QMUI.h" +#import "QMUICore.h" +#import "UIView+QMUI.h" @implementation UIGestureRecognizer (QMUI) ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation([UIGestureRecognizer class], @selector(setEnabled:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIGestureRecognizer *selfObject, BOOL firstArgv) { + + // 检测常见的错误,例如在 viewWillAppear: 里把系统手势返回禁用,会导致从下一个界面手势返回到当前界面的瞬间,手势返回无效,界面处于混乱状态,无法接受任何点击事件 + // _UIParallaxTransitionPanGestureRecognizer + if ([NSStringFromClass(selfObject.class) containsString:@"_UIParallaxTransition"] && selfObject.enabled && !firstArgv && (selfObject.state == UIGestureRecognizerStateBegan || selfObject.state == UIGestureRecognizerStateChanged)) { + NSString *desc = @"disabling interactivePopGestureRecognizer during its execution may lead to interface state inconsistency!"; + UINavigationController *navController = selfObject.view.qmui_viewController; + if ([navController isKindOfClass:UINavigationController.class]) { + UIViewController *fromVc = [navController.transitionCoordinator viewControllerForKey:UITransitionContextFromViewControllerKey]; + UIViewController *toVc = [navController.transitionCoordinator viewControllerForKey:UITransitionContextToViewControllerKey]; + if (fromVc || toVc) { + desc = [NSString stringWithFormat:@"%@ fromVc: %@, toVc: %@", desc, NSStringFromClass(fromVc.class), NSStringFromClass(toVc.class)]; + } + } + QMUIAssert(NO, @"UIGestureRecognizer (QMUI)", @"%@", desc); + } + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + }; + }); + }); +} + - (nullable UIView *)qmui_targetView { CGPoint location = [self locationInView:self.view]; UIView *targetView = [self.view hitTest:location withEvent:nil]; diff --git a/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.h index 1465c872..06af72f7 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIImage+QMUI.h @@ -18,17 +18,14 @@ #define CGContextInspectSize(size) [QMUIHelper inspectContextSize:size] -#ifdef DEBUG - #define CGContextInspectContext(context) [QMUIHelper inspectContextIfInvalidatedInDebugMode:context] -#else - #define CGContextInspectContext(context) if(![QMUIHelper inspectContextIfInvalidatedInReleaseMode:context]){return nil;} -#endif +#define CGContextInspectContext(context, returnValue) if(![QMUIHelper inspectContextIfInvalidated:context]){return returnValue;} +#define CGContextInspectContextReturnVoid(context) if(![QMUIHelper inspectContextIfInvalidated:context]){return;} NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSInteger, QMUIImageShape) { QMUIImageShapeOval, // 椭圆 - QMUIImageShapeTriangle, // 三角形 + QMUIImageShapeTriangle, // 尖头向上的三角形 QMUIImageShapeDisclosureIndicator, // 列表 cell 右边的箭头 QMUIImageShapeCheckmark, // 列表 cell 右边的checkmark QMUIImageShapeDetailButtonImage, // 列表 cell 右边的 i 按钮图片 @@ -52,6 +49,14 @@ typedef NS_ENUM(NSInteger, QMUIImageResizingMode) { QMUIImageResizingModeScaleAspectFillBottom // 将图片保持宽高比例不变的情况下缩放到不超过给定的大小(但缩放后的大小不一定与给定大小相等),若有内容超出则会被裁剪。若裁剪则水平居中、垂直居下裁剪。 }; +typedef NS_ENUM(NSInteger, QMUIImageGradientType) { + QMUIImageGradientTypeHorizontal, + QMUIImageGradientTypeVertical, + QMUIImageGradientTypeTopLeftToBottomRight, + QMUIImageGradientTypeTopRightToBottomLeft, + QMUIImageGradientTypeRadial, +}; + @interface UIImage (QMUI) /** @@ -65,6 +70,12 @@ typedef NS_ENUM(NSInteger, QMUIImageResizingMode) { */ + (nullable UIImage *)qmui_imageWithSize:(CGSize)size opaque:(BOOL)opaque scale:(CGFloat)scale actions:(void (^)(CGContextRef contextRef))actionBlock; +/// 获取当前图片在 ImageAsset 里的名字(若有),且即便经过 imageWithRenderingMode 转换后也依然可以正常保留该名字(系统默认转换后就丢失名字了) +@property(nonatomic, copy, readonly, nullable) NSString *qmui_name; + +/// 当前图片是否是可拉伸/平铺的,也即通过 resizableImageWithCapInsets: 处理过的图片 +@property(nonatomic, assign, readonly) BOOL qmui_resizable; + /// 获取当前图片的像素大小,如果是多倍图,会被放大到一倍来算 @property(nonatomic, assign, readonly) CGSize qmui_sizeInPixel; @@ -310,6 +321,16 @@ typedef NS_ENUM(NSInteger, QMUIImageResizingMode) { */ + (nullable UIImage *)qmui_imageWithColor:(nullable UIColor *)color size:(CGSize)size cornerRadiusArray:(nullable NSArray *)cornerRadius; +/** + 创建一个渐变图片,支持线性、径向。 + @param colors 渐变的颜色,不能为空,数量必须与 locations 数量一致(除非 locations 为 nil) + @param type 渐变的类型,可选为水平、垂直、径向、左上至右下、右上至左下 + @param locations 渐变变化的位置,数量必须与 colors 一致,值为 [0.0-1.0] 之间的 CGFloat。如果参数传 nil 则默认为 @[@0, @1] + @param size 图片的尺寸,如果是径向渐变,宽高不相等时会变成椭圆的渐变。 + @param cornerRadius 四个角的圆角值的数组,长度必须为4,顺序分别为[左上角、左下角、右下角、右上角] + */ ++ (nullable UIImage *)qmui_imageWithGradientColors:(NSArray *)colors type:(QMUIImageGradientType)type locations:(nullable NSArray *)locations size:(CGSize)size cornerRadiusArray:(nullable NSArray *)cornerRadius; + /** * 创建一个带边框路径,没有背景色的路径图片,border的路径为path * @@ -360,11 +381,6 @@ typedef NS_ENUM(NSInteger, QMUIImageResizingMode) { */ + (nullable UIImage *)qmui_imageWithShape:(QMUIImageShape)shape size:(CGSize)size lineWidth:(CGFloat)lineWidth tintColor:(nullable UIColor *)tintColor; -/** - * 将文字渲染成图片,最终图片和文字一样大 - */ -+ (nullable UIImage *)qmui_imageWithAttributedString:(NSAttributedString *)attributedString; - /** 对传进来的 `UIView` 截图,生成一个 `UIImage` 并返回。注意这里使用的是 view.layer 来渲染图片内容。 diff --git a/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.m index e9fab510..5e54ab01 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIImage+QMUI.m @@ -18,6 +18,10 @@ #import "UIBezierPath+QMUI.h" #import "UIColor+QMUI.h" #import "QMUILog.h" +#import "NSArray+QMUI.h" +#import "CALayer+QMUI.h" +#import +#import #import CG_INLINE CGSize @@ -38,12 +42,9 @@ + (void)load { OverrideImplementation([UIImage class], @selector(resizableImageWithCapInsets:resizingMode:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^UIImage *(UIImage *selfObject, UIEdgeInsets capInsets, UIImageResizingMode resizingMode) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (!CGSizeIsEmpty(selfObject.size) && (UIEdgeInsetsGetHorizontalValue(capInsets) >= selfObject.size.width || UIEdgeInsetsGetVerticalValue(capInsets) >= selfObject.size.height)) { - // 如果命中这个判断,请减小 capInsets 的值 - QMUILogWarn(@"UIImage (QMUI)", @"UIImage (QMUI) resizableImageWithCapInsets 传进来的 capInsets 的水平/垂直方向的和应该小于图片本身的大小,否则会导致 render 时出现 invalid context 0x0 的错误。"); - } + if (!CGSizeIsEmpty(selfObject.size) && (UIEdgeInsetsGetHorizontalValue(capInsets) >= selfObject.size.width || UIEdgeInsetsGetVerticalValue(capInsets) >= selfObject.size.height)) { + // 如果命中这个判断,请减小 capInsets 的值 + QMUILogWarn(@"UIImage (QMUI)", @"UIImage (QMUI) resizableImageWithCapInsets 传进来的 capInsets 的水平/垂直方向的和应该小于图片本身的大小,否则会导致 render 时出现 invalid context 0x0 的错误。"); } // call super @@ -54,6 +55,22 @@ + (void)load { return result; }; }); + + OverrideImplementation([UIImage class], @selector(imageWithRenderingMode:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UIImage *selfObject, UIImageRenderingMode mode) { + + // call super + UIImage * (*originSelectorIMP)(id, SEL, UIImageRenderingMode); + originSelectorIMP = (UIImage * (*)(id, SEL, UIImageRenderingMode))originalIMPProvider(); + UIImage * result = originSelectorIMP(selfObject, originCMD, mode); + + NSString *name = selfObject.qmui_name; + if (![result.qmui_name isEqualToString:name]) { + [result qmui_bindObject:name forKey:kQMUIImageNameKey]; + } + return result; + }; + }); }); } @@ -61,15 +78,43 @@ + (UIImage *)qmui_imageWithSize:(CGSize)size opaque:(BOOL)opaque scale:(CGFloat) if (!actionBlock || CGSizeIsEmpty(size)) { return nil; } - UIGraphicsBeginImageContextWithOptions(size, opaque, scale); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextInspectContext(context); - actionBlock(context); - UIImage *imageOut = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + UIGraphicsImageRendererFormat *format = [[UIGraphicsImageRendererFormat alloc] init]; + format.scale = scale; + format.opaque = opaque; + UIGraphicsImageRenderer *render = [[UIGraphicsImageRenderer alloc] initWithSize:size format:format]; + UIImage *imageOut = [render imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) { + CGContextRef context = rendererContext.CGContext; + CGContextInspectContextReturnVoid(context); + actionBlock(context); + }]; return imageOut; } +static NSString * const kQMUIImageNameKey = @"kQMUIImageNameKey"; +- (NSString *)qmui_name { + NSString *name = [self qmui_getBoundObjectForKey:kQMUIImageNameKey]; + if (name.length) { + return name; + } + UIImageAsset *asset = [self valueForKey:@"_imageAsset"];// UIImage.imageAsset 是懒加载的,如果当前 image 并非从 Asset 里获取的,直接访问 getter 也会导致它构造一个 UIImageAsset 对象出来,导致后续的 assetName 为随机字符串,所以这里通过 valueForKey: 的方式直接访问 Ivar + SEL selector = NSSelectorFromString(@"assetName"); + if ([asset respondsToSelector:selector]) { + BeginIgnorePerformSelectorLeaksWarning + name = [asset performSelector:selector]; + EndIgnorePerformSelectorLeaksWarning + if (name.length) { + return name; + } + } + return nil; +} + +- (BOOL)qmui_resizable { + BOOL result; + [self qmui_performSelector:NSSelectorFromString(@"_isResizable") withPrimitiveReturnValue:&result]; + return result; +} + - (CGSize)qmui_sizeInPixel { CGSize size = CGSizeMake(self.size.width * self.scale, self.size.height * self.scale); return size; @@ -87,7 +132,7 @@ - (UIColor *)qmui_averageColor { unsigned char rgba[4] = {}; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(rgba, 1, 1, 8, 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); - CGContextInspectContext(context); + CGContextInspectContext(context, nil); CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), self.CGImage); CGColorSpaceRelease(colorSpace); CGContextRelease(context); @@ -109,7 +154,7 @@ - (UIImage *)qmui_grayImage { CGSize size = self.qmui_sizeInPixel; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); CGContextRef context = CGBitmapContextCreate(NULL, size.width, size.height, 8, 0, colorSpace, kCGBitmapByteOrderDefault); - CGContextInspectContext(context); + CGContextInspectContext(context, nil); CGColorSpaceRelease(colorSpace); if (context == NULL) { return nil; @@ -133,7 +178,7 @@ - (UIImage *)qmui_grayImage { // 用 CGBitmapContextCreateImage 方式创建出来的图片,CGImageAlphaInfo 总是为 CGImageAlphaInfoNone,导致 qmui_opaque 与原图不一致,所以这里再做多一步 grayImage = [UIImage qmui_imageWithSize:grayImage.size opaque:NO scale:grayImage.scale actions:^(CGContextRef contextRef) { - [grayImage drawInRect:imageRect]; + [grayImage drawInRect:CGRectMakeWithSize(grayImage.size)]; }]; } @@ -149,14 +194,29 @@ - (UIImage *)qmui_imageWithAlpha:(CGFloat)alpha { } - (UIImage *)qmui_imageWithTintColor:(UIColor *)tintColor { - return [UIImage qmui_imageWithSize:self.size opaque:self.qmui_opaque scale:self.scale actions:^(CGContextRef contextRef) { + // iOS 13 的 imageWithTintColor: 方法里并不会去更新 CGImage,所以通过它更改了图片颜色后再获取到的 CGImage 依然是旧的,因此暂不使用 +// if (@available(iOS 13.0, *)) { +// return [self imageWithTintColor:tintColor]; +// } + BOOL opaque = self.qmui_opaque ? tintColor.qmui_alpha >= 1.0 : NO;// 如果图片不透明但 tintColor 半透明,则生成的图片也应该是半透明的 + UIImage *result = [UIImage qmui_imageWithSize:self.size opaque:opaque scale:self.scale actions:^(CGContextRef contextRef) { CGContextTranslateCTM(contextRef, 0, self.size.height); CGContextScaleCTM(contextRef, 1.0, -1.0); - CGContextSetBlendMode(contextRef, kCGBlendModeNormal); - CGContextClipToMask(contextRef, CGRectMakeWithSize(self.size), self.CGImage); + if (!opaque) { + CGContextSetBlendMode(contextRef, kCGBlendModeNormal); + CGContextClipToMask(contextRef, CGRectMakeWithSize(self.size), self.CGImage); + } CGContextSetFillColorWithColor(contextRef, tintColor.CGColor); CGContextFillRect(contextRef, CGRectMakeWithSize(self.size)); }]; + + SEL selector = NSSelectorFromString(@"qmui_generatorSupportsDynamicColor"); + if ([NSStringFromClass(tintColor.class) containsString:@"QMUIThemeColor"] && [UIImage respondsToSelector:selector]) { + BOOL supports; + [UIImage.class qmui_performSelector:selector withPrimitiveReturnValue:&supports]; + QMUIAssert(supports, @"UIImage (QMUI)", @"UIImage (QMUITheme) hook 尚未生效,QMUIThemeColor 生成的图片无法自动转成 QMUIThemeImage,可能导致 theme 切换时无法刷新。"); + } + return result; } - (UIImage *)qmui_imageWithBlendColor:(UIColor *)blendColor { @@ -428,15 +488,16 @@ + (UIImage *)qmui_animatedImageWithData:(NSData *)data scale:(CGFloat)scale { CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); size_t count = CGImageSourceGetCount(source); UIImage *animatedImage = nil; + scale = scale == 0 ? ScreenScale : scale; if (count <= 1) { - animatedImage = [[UIImage alloc] initWithData:data]; + animatedImage = [[UIImage alloc] initWithData:data scale:scale]; } else { NSMutableArray *images = [[NSMutableArray alloc] init]; NSTimeInterval duration = 0.0f; for (size_t i = 0; i < count; i++) { CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL); duration += [self qmui_frameDurationAtIndex:i source:source]; - UIImage *frameImage = [UIImage imageWithCGImage:image scale:scale == 0 ? ScreenScale : scale orientation:UIImageOrientationUp]; + UIImage *frameImage = [UIImage imageWithCGImage:image scale:scale orientation:UIImageOrientationUp]; [images addObject:frameImage]; CGImageRelease(image); } @@ -542,7 +603,7 @@ + (UIImage *)qmui_imageWithColor:(UIColor *)color size:(CGSize)size cornerRadius color = color ? color : UIColorClear; BOOL opaque = (cornerRadius == 0.0 && [color qmui_alpha] == 1.0); - return [UIImage qmui_imageWithSize:size opaque:opaque scale:0 actions:^(CGContextRef contextRef) { + UIImage *result = [UIImage qmui_imageWithSize:size opaque:opaque scale:0 actions:^(CGContextRef contextRef) { CGContextSetFillColorWithColor(contextRef, color.CGColor); if (cornerRadius > 0) { @@ -553,12 +614,19 @@ + (UIImage *)qmui_imageWithColor:(UIColor *)color size:(CGSize)size cornerRadius CGContextFillRect(contextRef, CGRectMakeWithSize(size)); } }]; + SEL selector = NSSelectorFromString(@"qmui_generatorSupportsDynamicColor"); + if ([NSStringFromClass(color.class) containsString:@"QMUIThemeColor"] && [UIImage respondsToSelector:selector]) { + BOOL supports; + [UIImage.class qmui_performSelector:selector withPrimitiveReturnValue:&supports]; + QMUIAssert(supports, @"UIImage (QMUI)", @"UIImage (QMUITheme) hook 尚未生效,QMUIThemeColor 生成的图片无法自动转成 QMUIThemeImage,可能导致 theme 切换时无法刷新。"); + } + return result; } + (UIImage *)qmui_imageWithColor:(UIColor *)color size:(CGSize)size cornerRadiusArray:(NSArray *)cornerRadius { size = CGSizeFlatted(size); CGContextInspectSize(size); - color = color ? color : UIColorWhite; + color = color ? color : [UIColor colorWithRed:1 green:1 blue:1 alpha:1]; return [UIImage qmui_imageWithSize:size opaque:NO scale:0 actions:^(CGContextRef contextRef) { CGContextSetFillColorWithColor(contextRef, color.CGColor); @@ -569,6 +637,65 @@ + (UIImage *)qmui_imageWithColor:(UIColor *)color size:(CGSize)size cornerRadius }]; } ++ (UIImage *)qmui_imageWithGradientColors:(NSArray *)colors type:(QMUIImageGradientType)type locations:(NSArray *)locations size:(CGSize)size cornerRadiusArray:(NSArray *)cornerRadius { + size = CGSizeFlatted(size); + CGContextInspectSize(size); + locations = locations ?: @[@0, @1]; + QMUIAssert(type != QMUIImageGradientTypeRadial || (type == QMUIImageGradientTypeRadial && locations.count == 2), @"UIImage (QMUI)", @"QMUIImageGradientTypeRadial 只能与2个 location 搭配使用,目前 locations 为 %@ 个", @(locations.count)); + return [UIImage qmui_imageWithSize:size opaque:NO scale:0 actions:^(CGContextRef _Nonnull contextRef) { + if (cornerRadius) { + UIBezierPath *path = [UIBezierPath qmui_bezierPathWithRoundedRect:CGRectMakeWithSize(size) cornerRadiusArray:cornerRadius lineWidth:0]; + [path addClip]; + } + + // 这里不用 CAGradientLayer 来渲染,因为发现实际效果会产生一些色差,暂不清楚为什么,所以只能用 Core Graphic 渲染 + CGColorSpaceRef spaceRef = CGColorSpaceCreateDeviceRGB(); + CGFloat cLocations[locations.count]; + for (NSInteger i = 0; i < locations.count; i++) { + cLocations[i] = locations[i].qmui_CGFloatValue; + } + + CGGradientRef gradient = CGGradientCreateWithColors(spaceRef, (CFArrayRef)[colors qmui_mapWithBlock:^id _Nonnull(UIColor * _Nonnull item, NSInteger index) { + return (id)item.CGColor; + }], cLocations); + if (type == QMUIImageGradientTypeRadial) { + CGFloat minSize = MIN(size.width, size.height); + CGFloat radius = minSize / 2; + CGFloat horizontalRatio = size.width / minSize; + CGFloat verticalRatio = size.height / minSize; + // 缩放是为了让渐变的圆形可以按照 size 变成椭圆的 + CGContextTranslateCTM(contextRef, -(horizontalRatio - 1) * size.width / 2, -(verticalRatio - 1) * size.height / 2); + CGContextScaleCTM(contextRef, horizontalRatio, verticalRatio); + CGContextDrawRadialGradient(contextRef, + gradient, + CGPointMake(size.width / 2, size.height / 2), + 0, + CGPointMake(size.width / 2, size.height / 2), + radius, + kCGGradientDrawsBeforeStartLocation); + } else { + CGPoint startPoint = CGPointZero; + CGPoint endPoint = CGPointZero; + if (type == QMUIImageGradientTypeHorizontal) { + startPoint = CGPointMake(0, 0); + endPoint = CGPointMake(size.width, 0); + } else if(type == QMUIImageGradientTypeVertical) { + startPoint = CGPointMake(0, 0); + endPoint = CGPointMake(0, size.height); + }else if (type == QMUIImageGradientTypeTopLeftToBottomRight){ + startPoint = CGPointMake(0, 0); + endPoint = CGPointMake(size.width, size.height); + }else if (type == QMUIImageGradientTypeTopRightToBottomLeft){ + startPoint = CGPointMake(size.width, 0); + endPoint = CGPointMake(0, size.height); + } + CGContextDrawLinearGradient(contextRef, gradient, startPoint, endPoint, kCGGradientDrawsBeforeStartLocation); + } + CGColorSpaceRelease(spaceRef); + CGGradientRelease(gradient); + }]; +} + + (UIImage *)qmui_imageWithShape:(QMUIImageShape)shape size:(CGSize)size lineWidth:(CGFloat)lineWidth tintColor:(UIColor *)tintColor { size = CGSizeFlatted(size); CGContextInspectSize(size); @@ -687,14 +814,6 @@ + (UIImage *)qmui_imageWithShape:(QMUIImageShape)shape size:(CGSize)size tintCol return [UIImage qmui_imageWithShape:shape size:size lineWidth:lineWidth tintColor:tintColor]; } -+ (UIImage *)qmui_imageWithAttributedString:(NSAttributedString *)attributedString { - CGSize stringSize = [attributedString boundingRectWithSize:CGSizeMax options:NSStringDrawingUsesLineFragmentOrigin context:nil].size; - stringSize = CGSizeCeil(stringSize); - return [UIImage qmui_imageWithSize:stringSize opaque:NO scale:0 actions:^(CGContextRef contextRef) { - [attributedString drawInRect:CGRectMakeWithSize(stringSize)]; - }]; -} - + (UIImage *)qmui_imageWithView:(UIView *)view { CGContextInspectSize(view.bounds.size); return [UIImage qmui_imageWithSize:view.bounds.size opaque:NO scale:0 actions:^(CGContextRef contextRef) { diff --git a/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.h index a2944ca7..32343901 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIImageView+QMUI.h @@ -24,7 +24,7 @@ @property(nonatomic, assign) BOOL qmui_pause; /** - 是否要用 QMUI 提供的高性能方式去渲染由 [UIImage animatedImage] 创建的 UIImage,(系统原生的方式在 UIImageView 被放在 UIScrollView 内时会卡顿),默认为 YES。 + 是否要用 QMUI 提供的高性能方式去渲染由 [UIImage animatedImage] 创建的 UIImage,(系统原生的方式在 UIImageView 被放在 UIScrollView 内时会卡顿),默认为 NO。 */ @property(nonatomic, assign) BOOL qmui_smoothAnimation; diff --git a/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.m index aab20850..b79fe9af 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIImageView+QMUI.m @@ -20,6 +20,7 @@ @interface UIImageView () +@property(nonatomic, strong) CALayer *qimgv_animatedImageLayer; @property(nonatomic, strong) CADisplayLink *qimgv_displayLink; @property(nonatomic, strong) UIImage *qimgv_animatedImage; @property(nonatomic, assign) NSInteger qimgv_currentAnimatedImageIndex; @@ -27,34 +28,13 @@ @interface UIImageView () @implementation UIImageView (QMUI) +QMUISynthesizeIdStrongProperty(qimgv_animatedImageLayer, setQimgv_animatedImageLayer) QMUISynthesizeIdStrongProperty(qimgv_displayLink, setQimgv_displayLink) QMUISynthesizeIdStrongProperty(qimgv_animatedImage, setQimgv_animatedImage) QMUISynthesizeNSIntegerProperty(qimgv_currentAnimatedImageIndex, setQimgv_currentAnimatedImageIndex) -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - - // 虽然 UIImageView.h 里并没有把两个 initWithImage: 方法标记为 NS_DESIGNATED_INITIALIZER,但实测它们就是 NS_DESIGNATED_INITIALIZER,不会互相调用,也不会调用 initWithFrame:、initWithCoder: - ExtendImplementationOfNonVoidMethodWithSingleArgument([UIImageView class], @selector(initWithImage:), UIImage *, UIImageView *, ^UIImageView *(UIImageView *selfObject, UIImage *image, UIImageView *originReturnValue) { - [selfObject qimgv_didInitialize]; - return originReturnValue; - }); - ExtendImplementationOfNonVoidMethodWithTwoArguments([UIImageView class], @selector(initWithImage:highlightedImage:), UIImage *, UIImage *, UIImageView *, ^UIImageView *(UIImageView *selfObject, UIImage *image, UIImage *highlightedImage, UIImageView *originReturnValue) { - [selfObject qimgv_didInitialize]; - return originReturnValue; - }); - - ExtendImplementationOfNonVoidMethodWithSingleArgument([UIImageView class], @selector(initWithFrame:), CGRect, UIImageView *, ^UIImageView *(UIImageView *selfObject, CGRect frame, UIImageView *originReturnValue) { - [selfObject qimgv_didInitialize]; - return originReturnValue; - }); - - ExtendImplementationOfNonVoidMethodWithSingleArgument([UIImageView class], @selector(initWithCoder:), NSCoder *, UIImageView *, ^UIImageView *(UIImageView *selfObject, NSCoder *aDecoder, UIImageView *originReturnValue) { - [selfObject qimgv_didInitialize]; - return originReturnValue; - }); - +- (void)qimgv_swizzleMethods { + [QMUIHelper executeBlock:^{ OverrideImplementation([UIImageView class], @selector(setImage:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIImageView *selfObject, UIImage *image) { @@ -65,20 +45,15 @@ + (void)load { originSelectorIMP(selfObject, originCMD, aImage); }; - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (selfObject.qmui_smoothAnimation && image.images) { - if (image != selfObject.qimgv_animatedImage) { - callSuperBlock(nil); - selfObject.qimgv_animatedImage = image; - [selfObject qimgv_requestToStartAnimation]; - } - } else { - selfObject.qimgv_animatedImage = nil; - [selfObject qimgv_stopAnimating]; - callSuperBlock(image); + if (selfObject.qmui_smoothAnimation && image.images) { + if (image != selfObject.qimgv_animatedImage) { + callSuperBlock(nil); + selfObject.qimgv_animatedImage = image; + [selfObject qimgv_requestToStartAnimation]; } } else { + selfObject.qimgv_animatedImage = nil; + [selfObject qimgv_stopAnimating]; callSuperBlock(image); } }; @@ -86,11 +61,8 @@ + (void)load { OverrideImplementation([UIImageView class], @selector(image), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^UIImage *(UIImageView *selfObject) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (selfObject.qimgv_animatedImage) { - return selfObject.qimgv_animatedImage; - } + if (selfObject.qimgv_animatedImage) { + return selfObject.qimgv_animatedImage; } // call super @@ -101,23 +73,11 @@ + (void)load { return result; }; }); - - OverrideImplementation([UIImageView class], @selector(displayLayer:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIImageView *selfObject, CALayer *layer) { - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (selfObject.qimgv_animatedImage) { - layer.contents = (__bridge id)selfObject.qimgv_animatedImage.images[selfObject.qimgv_currentAnimatedImageIndex].CGImage; - return; - } - } - - // call super - void (*originSelectorIMP)(id, SEL, CALayer *); - originSelectorIMP = (void (*)(id, SEL, CALayer *))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, layer); - }; + + ExtendImplementationOfVoidMethodWithoutArguments([UIImageView class], @selector(layoutSubviews), ^(UIImageView *selfObject) { + if (selfObject.qimgv_animatedImageLayer) { + selfObject.qimgv_animatedImageLayer.frame = selfObject.bounds; + } }); ExtendImplementationOfVoidMethodWithoutArguments([UIImageView class], @selector(didMoveToWindow), ^(UIImageView *selfObject) { @@ -139,11 +99,8 @@ + (void)load { OverrideImplementation([UIImageView class], @selector(sizeThatFits:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^CGSize(UIImageView *selfObject, CGSize size) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (selfObject.qimgv_animatedImage) { - return selfObject.qimgv_animatedImage.size; - } + if (selfObject.qimgv_animatedImage) { + return selfObject.qimgv_animatedImage.size; } // call super @@ -153,27 +110,31 @@ + (void)load { return result; }; }); - }); -} - -- (void)qimgv_didInitialize { - self.qmui_smoothAnimation = YES; + + ExtendImplementationOfVoidMethodWithSingleArgument([UIImageView class], @selector(setContentMode:), UIViewContentMode, ^(UIImageView *selfObject, UIViewContentMode firstArgv) { + if (selfObject.qimgv_animatedImageLayer) { + selfObject.qimgv_animatedImageLayer.contentsGravity = [QMUIHelper layerContentsGravityWithContentMode:firstArgv]; + } + }); + } oncePerIdentifier:@"UIImageView (QMUI) smoothAnimation"]; } - (BOOL)qimgv_requestToStartAnimation { if (![self qimgv_canStartAnimation]) return NO; + if (!self.qimgv_animatedImageLayer) { + self.qimgv_animatedImageLayer = [CALayer layer]; + self.qimgv_animatedImageLayer.contentsGravity = [QMUIHelper layerContentsGravityWithContentMode:self.contentMode]; + [self.layer addSublayer:self.qimgv_animatedImageLayer]; + } + if (!self.qimgv_displayLink) { self.qimgv_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)]; [self.qimgv_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; NSInteger preferredFramesPerSecond = self.qimgv_animatedImage.images.count / self.qimgv_animatedImage.duration; - if (@available(iOS 10, *)) { - self.qimgv_displayLink.preferredFramesPerSecond = preferredFramesPerSecond; - } else { - self.qimgv_displayLink.frameInterval = preferredFramesPerSecond; - } + self.qimgv_displayLink.preferredFramesPerSecond = preferredFramesPerSecond; self.qimgv_currentAnimatedImageIndex = -1; - self.layer.contents = (__bridge id)self.qimgv_animatedImage.images.firstObject.CGImage;// 对于那种一开始就 pause 的图,displayLayer: 不会被调用,所以看不到图,为了避免这种情况,手动先把第一帧显示出来 + self.qimgv_animatedImageLayer.contents = (__bridge id)self.qimgv_animatedImage.images.firstObject.CGImage;// 对于那种一开始就 pause 的图,displayLayer: 不会被调用,所以看不到图,为了避免这种情况,手动先把第一帧显示出来 } self.qimgv_displayLink.paused = self.qmui_pause; @@ -186,6 +147,10 @@ - (void)qimgv_stopAnimating { [self.qimgv_displayLink invalidate]; self.qimgv_displayLink = nil; } + if (self.qimgv_animatedImageLayer) { + [self.qimgv_animatedImageLayer removeFromSuperlayer]; + self.qimgv_animatedImageLayer = nil; + } } - (void)qimgv_updateAnimationStateAutomatically { @@ -202,12 +167,15 @@ - (BOOL)qimgv_canStartAnimation { - (void)handleDisplayLink:(CADisplayLink *)displayLink { self.qimgv_currentAnimatedImageIndex = self.qimgv_currentAnimatedImageIndex < self.qimgv_animatedImage.images.count - 1 ? (self.qimgv_currentAnimatedImageIndex + 1) : 0; - [self.layer setNeedsDisplay]; + self.qimgv_animatedImageLayer.contents = (__bridge id)self.qimgv_animatedImage.images[self.qimgv_currentAnimatedImageIndex].CGImage; } static char kAssociatedObjectKey_smoothAnimation; - (void)setQmui_smoothAnimation:(BOOL)qmui_smoothAnimation { objc_setAssociatedObject(self, &kAssociatedObjectKey_smoothAnimation, @(qmui_smoothAnimation), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_smoothAnimation) { + [self qimgv_swizzleMethods]; + } if (qmui_smoothAnimation && self.image.images && self.image != self.qimgv_animatedImage) { self.image = self.image;// 重新设置图片,触发动画 } else if (!qmui_smoothAnimation && self.qimgv_animatedImage) { @@ -223,7 +191,7 @@ - (BOOL)qmui_smoothAnimation { - (void)setQmui_pause:(BOOL)qmui_pause { objc_setAssociatedObject(self, &kAssociatedObjectKey_pause, @(qmui_pause), OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (self.animationImages || self.image.images) { - self.layer.qmui_pause = qmui_pause; + self.qimgv_animatedImageLayer.qmui_pause = qmui_pause; } if (self.qimgv_displayLink) { self.qimgv_displayLink.paused = qmui_pause; diff --git a/QMUI/QMUIKit/UIKitExtensions/UIInterface+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIInterface+QMUI.h index 4d90d8b5..241e720a 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIInterface+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIInterface+QMUI.h @@ -1,17 +1,16 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIInterface+QMUI.h // QMUIKit // // Created by QMUI Team on 2018/12/20. -// Copyright © 2018 QMUI Team. All rights reserved. // #import @@ -22,17 +21,9 @@ NS_ASSUME_NONNULL_BEGIN @interface QMUIHelper (QMUI_Interface) /** - * 旋转当前设备的方向到指定方向,一般用于 [UIViewController supportedInterfaceOrientations] 发生变化时主动触发界面方向的刷新 - * @return 是否真正旋转了方向,YES 表示参数的方向和目前设备方向不一致,NO 表示一致也即不会旋转 - * @see [QMUIConfiguration automaticallyRotateDeviceOrientation] - */ -+ (BOOL)rotateToDeviceOrientation:(UIDeviceOrientation)orientation; - -/** - * 记录手动旋转方向前的设备方向,当值不为 UIDeviceOrientationUnknown 时表示设备方向有经过了手动调整。默认值为 UIDeviceOrientationUnknown。 - * @see [QMUIHelper rotateToDeviceOrientation] + * 内部使用,记录手动旋转方向前的设备方向,当值不为 UIDeviceOrientationUnknown 时表示设备方向有经过了手动调整。默认值为 UIDeviceOrientationUnknown。 */ -@property(nonatomic, assign) UIDeviceOrientation orientationBeforeChangingByHelper; +@property(nonatomic, assign) UIDeviceOrientation lastOrientationChangedByHelper; /// 将一个 UIInterfaceOrientationMask 转换成对应的 UIDeviceOrientation + (UIDeviceOrientation)deviceOrientationWithInterfaceOrientationMask:(UIInterfaceOrientationMask)mask; @@ -57,4 +48,26 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface UIViewController (QMUI_Interface) + +/** + 尝试将手机旋转为指定方向。请确保传进来的参数属于 -[UIViewController supportedInterfaceOrientations] 返回的范围内,如不在该范围内会旋转失败。 + @return 旋转成功则返回 YES,旋转失败返回 NO。 + @note 请注意与 @c qmui_setNeedsUpdateOfSupportedInterfaceOrientations 的区别:如果你的界面支持N个方向,而你希望保持对这N个方向的支持的情况下把设备方向旋转为这N个方向里的某一个时,应该调用 @c qmui_rotateToInterfaceOrientation: 。如果你的界面支持N个方向,而某些情况下你希望把N换成M并触发设备的方向刷新,则请修改方向后,调用 @c qmui_setNeedsUpdateOfSupportedInterfaceOrientations 。更详细可查看:https://github.com/Tencent/QMUI_iOS/wiki/%E9%80%82%E7%94%A8%E4%BA%8E-iOS-16-%E5%8F%8A%E4%BB%A5%E4%B8%8B%E7%89%88%E6%9C%AC%E7%9A%84%E5%B1%8F%E5%B9%95%E6%96%B9%E5%90%91%E6%8E%A7%E5%88%B6%E6%96%B9%E5%BC%8F + */ +- (BOOL)qmui_rotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation; + +/** + 告知系统当前界面的方向有变化,需要刷新。通常在 -[UIViewController supportedInterfaceOrientations] 的值变化后调用,可无脑取代 iOS 16 的同名系统方法。 + */ +- (void)qmui_setNeedsUpdateOfSupportedInterfaceOrientations; + +/** + 在配置表 AutomaticallyRotateDeviceOrientation 功能开启的情况下,QMUI 会自动判断当前的 UIViewController 是否具备强制旋转设备方向的权利,而如果 QMUI 判断结果为没权利但你又希望当前的 UIViewController 具备这个权利,则可以重写该方法并返回 YES。 + 默认返回 NO,也即交给 QMUI 自动判断。 + @warning 该方法仅在 iOS 15 及以前版本有效,iOS 16 及以后版本交给系统处理,QMUI 不干涉。 + */ +- (BOOL)qmui_shouldForceRotateDeviceOrientation; +@end + NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIInterface+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIInterface+QMUI.m index f469c963..18b01795 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIInterface+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIInterface+QMUI.m @@ -1,17 +1,16 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIInterface+QMUI.m // QMUIKit // // Created by QMUI Team on 2018/12/20. -// Copyright © 2018 QMUI Team. All rights reserved. // #import "UIInterface+QMUI.h" @@ -19,30 +18,24 @@ @implementation QMUIHelper (QMUI_Interface) -QMUISynthesizeNSIntegerProperty(orientationBeforeChangingByHelper, setOrientationBeforeChangingByHelper) +QMUISynthesizeNSIntegerProperty(lastOrientationChangedByHelper, setLastOrientationChangedByHelper) - (void)handleDeviceOrientationNotification:(NSNotification *)notification { - // 如果是由 setValue:forKey: 方式修改方向而走到这个 notification 的话,理论上是不需要重置为 Unknown 的,但因为在 UIViewController (QMUI) 那边会再次记录旋转前的值,所以这里就算重置也无所谓 - [QMUIHelper sharedInstance].orientationBeforeChangingByHelper = UIDeviceOrientationUnknown; -} - -+ (BOOL)rotateToDeviceOrientation:(UIDeviceOrientation)orientation { - if ([UIDevice currentDevice].orientation == orientation) { - [UIViewController attemptRotationToDeviceOrientation]; - return NO; - } + QMUILogInfo(@"Interface (QMUI)", @"device orientation did change to %@", @(((UIDevice *)([notification.object isKindOfClass:UIDevice.class] ? notification.object : UIDevice.currentDevice)).orientation)); - [[UIDevice currentDevice] setValue:@(orientation) forKey:@"orientation"]; - return YES; + // 如果是由 setValue:forKey: 方式修改方向而走到这个 notification 的话,理论上是不需要重置为 Unknown 的,但因为在 UIViewController (QMUI) 那边会再次记录旋转前的值,所以这里就算重置也无所谓 + [QMUIHelper sharedInstance].lastOrientationChangedByHelper = UIDeviceOrientationUnknown; } + (UIDeviceOrientation)deviceOrientationWithInterfaceOrientationMask:(UIInterfaceOrientationMask)mask { - if ((mask & UIInterfaceOrientationMaskAll) == UIInterfaceOrientationMaskAll) { - return [UIDevice currentDevice].orientation; - } - if ((mask & UIInterfaceOrientationMaskAllButUpsideDown) == UIInterfaceOrientationMaskAllButUpsideDown) { - return [UIDevice currentDevice].orientation; + if (UIDevice.currentDevice.orientation == UIDeviceOrientationUnknown) return UIDeviceOrientationUnknown; + + // mask 包含多个方向值,如果要转换的 mask 方向已经包含当前设备方向,则直接返回当前设备方向,以免外面要用这个返回值去做方向旋转时出现不必要的旋转。 + UIInterfaceOrientationMask orientation = 1 << (UIInterfaceOrientation)UIDevice.currentDevice.orientation; + if (mask & orientation) { + return UIDevice.currentDevice.orientation; } + if ((mask & UIInterfaceOrientationMaskPortrait) == UIInterfaceOrientationMaskPortrait) { return UIDeviceOrientationPortrait; } @@ -116,7 +109,7 @@ + (CGFloat)angleForTransformWithInterfaceOrientation:(UIInterfaceOrientation)ori } + (CGAffineTransform)transformForCurrentInterfaceOrientation { - return [QMUIHelper transformWithInterfaceOrientation:[[UIApplication sharedApplication] statusBarOrientation]]; + return [QMUIHelper transformWithInterfaceOrientation:UIApplication.sharedApplication.statusBarOrientation]; } + (CGAffineTransform)transformWithInterfaceOrientation:(UIInterfaceOrientation)orientation { @@ -124,3 +117,110 @@ + (CGAffineTransform)transformWithInterfaceOrientation:(UIInterfaceOrientation)o return CGAffineTransformMakeRotation(angle); } @end + +@implementation UIViewController (QMUI_Interface) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // iOS 16 及以后,系统会在界面切换时自动旋转设备方向,所以不需要以下逻辑。 + if (@available(iOS 16.0, *)) return; + + // 实现 AutomaticallyRotateDeviceOrientation 开关的功能 + OverrideImplementation([UIViewController class], @selector(viewWillAppear:), ^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 (!AutomaticallyRotateDeviceOrientation) { + return; + } + + // 某些情况下的 UIViewController 不具备决定设备方向的权利,具体请看 https://github.com/Tencent/QMUI_iOS/issues/291 + if (![selfObject qmui_shouldForceRotateDeviceOrientation]) { + BOOL isRootViewController = [selfObject isViewLoaded] && selfObject.view.window.rootViewController == selfObject; + BOOL isChildViewController = [selfObject.tabBarController.viewControllers containsObject:selfObject] || [selfObject.navigationController.viewControllers containsObject:selfObject] || [selfObject.splitViewController.viewControllers containsObject:selfObject]; + BOOL hasRightsOfRotateDeviceOrientaion = isRootViewController || isChildViewController; + if (!hasRightsOfRotateDeviceOrientaion) { + return; + } + } + + + UIInterfaceOrientation statusBarOrientation = UIApplication.sharedApplication.statusBarOrientation; + UIDeviceOrientation lastOrientationChangedByHelper = [QMUIHelper sharedInstance].lastOrientationChangedByHelper; + BOOL shouldConsiderLastChanged = lastOrientationChangedByHelper != UIDeviceOrientationUnknown; + UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation; + + // 虽然这两者的 unknow 值是相同的,但在启动 App 时可能只有其中一个是 unknown + if (statusBarOrientation == UIInterfaceOrientationUnknown || deviceOrientation == UIDeviceOrientationUnknown) return; + + // 之前没用私有接口修改过,那就按最标准的方式去旋转 + if (!shouldConsiderLastChanged) { + // 如果当前设备方向和界面支持的方向不一致,则主动进行旋转 + UIDeviceOrientation deviceOrientationToRotate = [QMUIHelper interfaceOrientationMask:selfObject.supportedInterfaceOrientations containsDeviceOrientation:deviceOrientation] ? deviceOrientation : [QMUIHelper deviceOrientationWithInterfaceOrientationMask:selfObject.supportedInterfaceOrientations]; + if ([selfObject qmui_rotateToInterfaceOrientation:(UIInterfaceOrientation)deviceOrientationToRotate]) { + [QMUIHelper sharedInstance].lastOrientationChangedByHelper = deviceOrientation; + } else { + [QMUIHelper sharedInstance].lastOrientationChangedByHelper = UIDeviceOrientationUnknown; + } + return; + } + + // 用私有接口修改过方向,但下一个界面和当前界面方向不相同,则要把修改前记录下来的那个设备方向考虑进来 + UIDeviceOrientation deviceOrientationToRotate = [QMUIHelper interfaceOrientationMask:selfObject.supportedInterfaceOrientations containsDeviceOrientation:lastOrientationChangedByHelper] ? lastOrientationChangedByHelper : [QMUIHelper deviceOrientationWithInterfaceOrientationMask:selfObject.supportedInterfaceOrientations]; + [selfObject qmui_rotateToInterfaceOrientation:(UIInterfaceOrientation)deviceOrientationToRotate]; + }; + }); + }); +} + +- (BOOL)qmui_rotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + QMUILogInfo(@"Interface (QMUI)", @"try rotating to %@", @(interfaceOrientation)); + +#ifdef IOS16_SDK_ALLOWED + if (@available(iOS 16.0, *)) { + + [self setNeedsUpdateOfSupportedInterfaceOrientations]; + + __block BOOL result = YES; + UIInterfaceOrientationMask mask = 1 << interfaceOrientation; + UIWindow *window = self.view.window ?: UIApplication.sharedApplication.delegate.window; + [window.windowScene requestGeometryUpdateWithPreferences:[[UIWindowSceneGeometryPreferencesIOS alloc] initWithInterfaceOrientations:mask] errorHandler:^(NSError * _Nonnull error) { + if (error) { + result = NO; + } + }]; + return result; + } +#endif + + if ([UIDevice currentDevice].orientation == (UIDeviceOrientation)interfaceOrientation) { + [UIViewController attemptRotationToDeviceOrientation]; + return NO; + } + [[UIDevice currentDevice] setValue:@(interfaceOrientation) forKey:@"orientation"]; + return YES; +} + +- (void)qmui_setNeedsUpdateOfSupportedInterfaceOrientations { +#ifdef IOS16_SDK_ALLOWED + if (@available(iOS 16.0, *)) { + [self setNeedsUpdateOfSupportedInterfaceOrientations]; + } else +#endif + { + UIDeviceOrientation orientation = [QMUIHelper deviceOrientationWithInterfaceOrientationMask:self.supportedInterfaceOrientations]; + [[UIDevice currentDevice] setValue:@(orientation) forKey:@"orientation"]; + } +} + +- (BOOL)qmui_shouldForceRotateDeviceOrientation { + return NO; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UILabel+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UILabel+QMUI.h index 1bf1deca..17995ffc 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UILabel+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UILabel+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UILabel+QMUI.h @@ -17,6 +17,8 @@ NS_ASSUME_NONNULL_BEGIN +extern const CGFloat QMUILineHeightIdentity; + @interface UILabel (QMUI) - (instancetype)qmui_initWithFont:(nullable UIFont *)font textColor:(nullable UIColor *)textColor; @@ -44,7 +46,7 @@ NS_ASSUME_NONNULL_BEGIN * @note 如果同时通过 qmui_textAttributes 或 attributedText 给整段文字设置了行高,则此方法将不再生效。换句话说,此方法设置的行高将永远不会覆盖 qmui_textAttributes 或 attributedText 设置的行高。 * @note 比如对于字符串"abc",你通过 attributedText 设置 {0, 1} 这个 range 范围内的行高为 10,又通过 setQmui_lineHeight: 设置了整体行高为 20,则最终 {0, 1} 内的行高将为 10,而 {1, 2} 内的行高将为全局行高 20 * @note 比如对于字符串"abc",你先通过 setQmui_lineHeight: 设置整体行高为 10,又通过 attributedText/qmui_textAttributes 设置整体行高为 20,无论这两个设置的代码的先后顺序如何,最终行高都将为 20 - * + * @note 你可以通过设置 'QMUILineHeightIdentity' 来恢复 UILabel 默认的行高 * @note 当你设置了此属性后,每次你调用 setText: 时,其实都会被自动转而调用 setAttributedText: * * ----------------------------------- @@ -56,9 +58,20 @@ NS_ASSUME_NONNULL_BEGIN * @warning 除上述情况外,计算的数值都可能不准确,会返回 0。 * */ - @property(nonatomic, assign) CGFloat qmui_lineHeight; +/** + 获取当前 font.capHeight 的中心点在 label.bounds.size.height 里的y值(代表字符的中心点位置),从而令业务在试图将文本垂直居中时可以基于该属性的返回值去计算。 + @warning 仅对单行文本有意义,对 label 的高度没有要求,但 label 不应该调整过 baselineOffset + */ +@property(nonatomic, assign, readonly) CGFloat qmui_centerOfCapHeight; + +/** + 获取当前 font.xHeight 的中心点在 label.bounds.size.height 里的y值(代表x这种矮的字符的中心点位置),从而令业务在试图将文本垂直居中时可以基于该属性的返回值去计算。 + @warning 仅对单行文本有意义,对 label 的高度没有要求,但 label 不应该调整过 baselineOffset + */ +@property(nonatomic, assign, readonly) CGFloat qmui_centerOfXHeight; + /** * 将目标UILabel的样式属性设置到当前UILabel上 * @@ -81,4 +94,19 @@ NS_ASSUME_NONNULL_BEGIN @end +@interface UILabel (QMUI_Debug) + +/** + 调试功能,打开后会在 label 第一行文字里把 descender、xHeight、capHeight、lineHeight 所在的位置以线条的形式标记出来。 + 对这些属性的解释可以看这篇文章 https://www.rightpoint.com/rplabs/ios-tracking-typography + */ +@property(nonatomic, assign) BOOL qmui_showPrincipalLines; + +/** + 当打开 qmui_showPrincipalLines 时,通过这个属性控制线条的颜色,默认为 nil。 + 当该属性为 nil 时,将会用 UIColorTestRed 作为线条的颜色。 + */ +@property(nullable, nonatomic, strong) UIColor *qmui_principalLineColor; +@end + NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UILabel+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UILabel+QMUI.m index b1a57f97..8cbd04b9 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UILabel+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UILabel+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UILabel+QMUI.m @@ -18,34 +18,61 @@ #import "NSParagraphStyle+QMUI.h" #import "NSObject+QMUI.h" #import "NSNumber+QMUI.h" +#import "CALayer+QMUI.h" +#import "UIView+QMUI.h" + +const CGFloat QMUILineHeightIdentity = -1000; + +@interface UILabel () + +@property(nonatomic, strong) CAShapeLayer *qmuilb_principalLineLayer; +@end @implementation UILabel (QMUI) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ExchangeImplementations([self class], @selector(setText:), @selector(qmui_setText:)); - ExchangeImplementations([self class], @selector(setAttributedText:), @selector(qmui_setAttributedText:)); + SEL selectors[] = { + @selector(setText:), + @selector(setAttributedText:), + @selector(setLineBreakMode:), + @selector(setTextAlignment:), + }; + for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); index++) { + SEL originalSelector = selectors[index]; + SEL swizzledSelector = NSSelectorFromString([@"qmuilb_" stringByAppendingString:NSStringFromSelector(originalSelector)]); + ExchangeImplementations([UILabel class], originalSelector, swizzledSelector); + } }); } -- (void)qmui_setText:(NSString *)text { +- (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 qmui_setText:text]; + [self qmuilb_setText:text]; return; } if (!self.qmui_textAttributes.count && ![self _hasSetQmuiLineHeight]) { - [self qmui_setText:text]; + [self qmuilb_setText:text]; return; } NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:text attributes:self.qmui_textAttributes]; - [self qmui_setAttributedText:[self attributedStringWithKernAndLineHeightAdjusted:attributedString]]; + [self qmuilb_setAttributedText:[self attributedStringWithKernAndLineHeightAdjusted:attributedString]]; } // 在 qmui_textAttributes 样式基础上添加用户传入的 attributedString 中包含的新样式。换句话说,如果这个方法里有样式冲突,则以 attributedText 为准 -- (void)qmui_setAttributedText:(NSAttributedString *)text { +- (void)qmuilb_setAttributedText:(NSAttributedString *)text { if (!text || (!self.qmui_textAttributes.count && ![self _hasSetQmuiLineHeight])) { - [self qmui_setAttributedText:text]; + [self qmuilb_setAttributedText:text]; return; } NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text.string attributes:self.qmui_textAttributes]; @@ -53,7 +80,7 @@ - (void)qmui_setAttributedText:(NSAttributedString *)text { [text enumerateAttributesInRange:NSMakeRange(0, text.length) options:0 usingBlock:^(NSDictionary * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { [attributedString addAttributes:attrs range:range]; }]; - [self qmui_setAttributedText:attributedString]; + [self qmuilb_setAttributedText:attributedString]; } static char kAssociatedObjectKey_textAttributes; @@ -78,7 +105,7 @@ - (void)setQmui_textAttributes:(NSDictionary *)qmui_t NSMutableArray *willRemovedAttributes = [NSMutableArray array]; [string enumerateAttributesInRange:NSMakeRange(0, string.length) options:0 usingBlock:^(NSDictionary * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { // 如果存在 kern 属性,则只有 range 是第一个字至倒数第二个字,才有可能是通过 qmui_textAttribtus 设置的 - if (NSEqualRanges(range, NSMakeRange(0, string.length - 1)) && [attrs[NSKernAttributeName] isEqualToNumber:prevTextAttributes[NSKernAttributeName]]) { + if (NSEqualRanges(range, NSMakeRange(0, string.length - 1)) && [attrs[NSKernAttributeName] isEqual:prevTextAttributes[NSKernAttributeName]]) { [string removeAttribute:NSKernAttributeName range:NSMakeRange(0, string.length - 1)]; } // 上面排除掉 kern 属性后,如果 range 不是整个字符串,那肯定不是通过 qmui_textAttributes 设置的 @@ -101,7 +128,7 @@ - (void)setQmui_textAttributes:(NSDictionary *)qmui_t [string addAttributes:qmui_textAttributes range:fullRange]; } // 不能调用 setAttributedText: ,否则若遇到样式冲突,那个方法会让用户传进来的 NSAttributedString 样式覆盖 qmui_textAttributes 的样式 - [self qmui_setAttributedText:[self attributedStringWithKernAndLineHeightAdjusted:string]]; + [self qmuilb_setAttributedText:[self attributedStringWithKernAndLineHeightAdjusted:string]]; } - (NSDictionary *)qmui_textAttributes { @@ -140,15 +167,46 @@ - (NSAttributedString *)attributedStringWithKernAndLineHeightAdjusted:(NSAttribu if (shouldAdjustLineHeight) { NSMutableParagraphStyle *paraStyle = [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:self.qmui_lineHeight lineBreakMode:self.lineBreakMode textAlignment:self.textAlignment]; [attributedString addAttribute:NSParagraphStyleAttributeName value:paraStyle range:NSMakeRange(0, attributedString.length)]; + + // iOS 默认文字底对齐,改了行高要自己调整才能保证文字一直在 label 里垂直居中 + CGFloat baselineOffset = [QMUIHelper baselineOffsetWhenVerticalAlignCenterInHeight:self.qmui_lineHeight withFont:self.font]; + [attributedString addAttribute:NSBaselineOffsetAttributeName value:@(baselineOffset) range:NSMakeRange(0, attributedString.length)]; } return attributedString; } +- (void)qmuilb_setLineBreakMode:(NSLineBreakMode)lineBreakMode { + [self qmuilb_setLineBreakMode:lineBreakMode]; + if (!self.qmui_textAttributes) return; + if (self.qmui_textAttributes[NSParagraphStyleAttributeName]) { + NSMutableParagraphStyle *p = ((NSParagraphStyle *)self.qmui_textAttributes[NSParagraphStyleAttributeName]).mutableCopy; + p.lineBreakMode = lineBreakMode; + NSMutableDictionary *attrs = self.qmui_textAttributes.mutableCopy; + attrs[NSParagraphStyleAttributeName] = p.copy; + self.qmui_textAttributes = attrs.copy; + } +} + +- (void)qmuilb_setTextAlignment:(NSTextAlignment)textAlignment { + [self qmuilb_setTextAlignment:textAlignment]; + if (!self.qmui_textAttributes) return; + if (self.qmui_textAttributes[NSParagraphStyleAttributeName]) { + NSMutableParagraphStyle *p = ((NSParagraphStyle *)self.qmui_textAttributes[NSParagraphStyleAttributeName]).mutableCopy; + p.alignment = textAlignment; + NSMutableDictionary *attrs = self.qmui_textAttributes.mutableCopy; + attrs[NSParagraphStyleAttributeName] = p.copy; + self.qmui_textAttributes = attrs.copy; + } +} + static char kAssociatedObjectKey_lineHeight; - (void)setQmui_lineHeight:(CGFloat)qmui_lineHeight { - objc_setAssociatedObject(self, &kAssociatedObjectKey_lineHeight, @(qmui_lineHeight), OBJC_ASSOCIATION_RETAIN_NONATOMIC); - + if (qmui_lineHeight == QMUILineHeightIdentity) { + objc_setAssociatedObject(self, &kAssociatedObjectKey_lineHeight, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } else { + objc_setAssociatedObject(self, &kAssociatedObjectKey_lineHeight, @(qmui_lineHeight), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } // 注意:对于 UILabel,只要你设置过 text,则 attributedText 就是有值的,因此这里无需区分 setText 还是 setAttributedText // 注意:这里需要刷新一下 qmui_textAttributes 对 text 的样式,否则刚进行设置的 lineHeight 就会无法设置。 NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:self.attributedText.string attributes:self.qmui_textAttributes]; @@ -175,6 +233,13 @@ - (CGFloat)qmui_lineHeight { return result == 0 ? self.font.lineHeight : result; } else if (self.text.length) { return self.font.lineHeight; + } else if (self.qmui_textAttributes) { + // 当前 label 连文字都没有时,再尝试从 qmui_textAttributes 里获取 + if ([self.qmui_textAttributes.allKeys containsObject:NSParagraphStyleAttributeName]) { + return ((NSParagraphStyle *)self.qmui_textAttributes[NSParagraphStyleAttributeName]).minimumLineHeight; + } else if ([self.qmui_textAttributes.allKeys containsObject:NSFontAttributeName]) { + return ((UIFont *)self.qmui_textAttributes[NSFontAttributeName]).lineHeight; + } } return 0; @@ -184,13 +249,24 @@ - (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; +} + +- (CGFloat)qmui_centerOfXHeight { + 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.xHeight / 2; + return center; } - (void)qmui_setTheSameAppearanceAsLabel:(UILabel *)label { @@ -219,3 +295,64 @@ - (void)qmui_avoidBlendedLayersIfShowingChineseWithBackgroundColor:(UIColor *)co } @end + +@implementation UILabel (QMUI_Debug) + +QMUISynthesizeIdStrongProperty(qmuilb_principalLineLayer, setQmuilb_principalLineLayer) +QMUISynthesizeIdStrongProperty(qmui_principalLineColor, setQmui_principalLineColor) + +static char kAssociatedObjectKey_showPrincipalLines; +- (void)setQmui_showPrincipalLines:(BOOL)qmui_showPrincipalLines { + objc_setAssociatedObject(self, &kAssociatedObjectKey_showPrincipalLines, @(qmui_showPrincipalLines), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_showPrincipalLines && !self.qmuilb_principalLineLayer) { + self.qmuilb_principalLineLayer = [CAShapeLayer layer]; + [self.qmuilb_principalLineLayer qmui_removeDefaultAnimations]; + self.qmuilb_principalLineLayer.strokeColor = (self.qmui_principalLineColor ?: UIColorTestRed).CGColor; + self.qmuilb_principalLineLayer.lineWidth = PixelOne; + [self.layer addSublayer:self.qmuilb_principalLineLayer]; + + 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; + + NSRange range = NSMakeRange(0, label.attributedText.length); + CGFloat lineOffset = [[label.attributedText attribute:NSBaselineOffsetAttributeName atIndex:0 effectiveRange:&range] doubleValue]; + // ≤ iOS 15 的设备上,1pt baseline 会让文本向上移动 2pt,≥ iOS 16 均为 1:1 移动。 + if (@available(iOS 16.0, *)) { + } else { + lineOffset = lineOffset * 2; + } + UIFont *font = label.font; + CGFloat maxX = CGRectGetWidth(label.bounds); + CGFloat maxY = CGRectGetHeight(label.bounds); + CGFloat descenderY = maxY + font.descender - lineOffset; + CGFloat xHeightY = maxY - (font.xHeight - font.descender) - lineOffset; + CGFloat capHeightY = maxY - (font.capHeight - font.descender) - lineOffset; + CGFloat lineHeightY = maxY - font.lineHeight - lineOffset; + + void (^addLineAtY)(UIBezierPath *, CGFloat) = ^void(UIBezierPath *p, CGFloat y) { + CGFloat offset = PixelOne / 2; + y = flat(y) - offset; + [p moveToPoint:CGPointMake(0, y)]; + [p addLineToPoint:CGPointMake(maxX, y)]; + }; + UIBezierPath *path = [UIBezierPath bezierPath]; + addLineAtY(path, descenderY); + addLineAtY(path, xHeightY); + addLineAtY(path, capHeightY); + addLineAtY(path, lineHeightY); + label.qmuilb_principalLineLayer.path = path.CGPath; + }; + } + } + self.qmuilb_principalLineLayer.hidden = !qmui_showPrincipalLines; +} + +- (BOOL)qmui_showPrincipalLines { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_showPrincipalLines)) boolValue]; +} + +@end diff --git a/QMUI/qmui/QMUIAppDelegate.h b/QMUI/QMUIKit/UIKitExtensions/UIMenuController+QMUI.h similarity index 71% rename from QMUI/qmui/QMUIAppDelegate.h rename to QMUI/QMUIKit/UIKitExtensions/UIMenuController+QMUI.h index b3feb251..9b36eaad 100644 --- a/QMUI/qmui/QMUIAppDelegate.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIMenuController+QMUI.h @@ -1,22 +1,23 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ - + */ // -// QMUIAppDelegate.h -// qmui +// UIMenuController+QMUI.h +// QMUIKit // -// Created by QMUI Team on 14-6-22. +// Created by 陈志宏 on 2019/7/21. // #import -@interface QMUIAppDelegate : UIResponder +NS_ASSUME_NONNULL_BEGIN -@property(strong, nonatomic) UIWindow *window; +@interface UIMenuController (QMUI) @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIMenuController+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIMenuController+QMUI.m new file mode 100644 index 00000000..572dba84 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIMenuController+QMUI.m @@ -0,0 +1,164 @@ +/** + * 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. + */ +// +// UIMenuController+QMUI.m +// QMUIKit +// +// Created by 陈志宏 on 2019/7/21. +// + +#import "UIMenuController+QMUI.h" +#import "QMUICore.h" +#import "NSArray+QMUI.h" + +@implementation UIMenuController (QMUI) + +static UIWindow *kMenuControllerWindow = nil; + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (@available(iOS 16.0, *)) { + // iOS 16 开始改为用 UIEditMenuInteraction,以前的做法也无效了,所以用 hook 的方式解决 + // https://github.com/Tencent/QMUI_iOS/issues/1538 + + // UIEditMenuInteraction + // - (void)presentEditMenuWithConfiguration:(UIEditMenuConfiguration *)configuration; + OverrideImplementation([UIEditMenuInteraction class], @selector(presentEditMenuWithConfiguration:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIEditMenuInteraction *selfObject, UIEditMenuConfiguration *configuration) { + + // call super + void (*originSelectorIMP)(id, SEL, UIEditMenuConfiguration *); + originSelectorIMP = (void (*)(id, SEL, UIEditMenuConfiguration *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, configuration); + + // 走到 present 的时候 window 可能还没构造,所以这里延迟一下再调用 + dispatch_async(dispatch_get_main_queue(), ^{ + [UIMenuController qmuimc_handleMenuWillShow]; + }); + }; + }); + + OverrideImplementation([UIEditMenuInteraction class], @selector(dismissMenu), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIEditMenuInteraction *selfObject) { + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + [UIMenuController qmuimc_handleMenuWillHide]; + }; + }); + + } else if (@available(iOS 13.0, *)) { + // +[UIMenuController sharedMenuController] + OverrideImplementation(object_getClass([UIMenuController class]), @selector(sharedMenuController), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIMenuController *selfObject) { + + // call super + UIMenuController *(*originSelectorIMP)(id, SEL); + originSelectorIMP = (UIMenuController *(*)(id, SEL))originalIMPProvider(); + UIMenuController *menuController = originSelectorIMP(selfObject, originCMD); + + /// 修复 issue:https://github.com/Tencent/QMUI_iOS/issues/659 + /// UIMenuController 本身就是单例,这里就不考虑释放了 + if (![menuController qmui_getBoundBOOLForKey:@"kHasAddedNotification"]) { + [menuController qmui_bindBOOL:YES forKey:@"kHasAddedNotification"]; + [NSNotificationCenter.defaultCenter addObserverForName:UIMenuControllerWillShowMenuNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull notification) { + [UIMenuController qmuimc_handleMenuWillShow]; + }]; + [NSNotificationCenter.defaultCenter addObserverForName:UIMenuControllerWillHideMenuNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull notification) { + [UIMenuController qmuimc_handleMenuWillHide]; + }]; + } + + return menuController; + }; + }); + } + }); +} + ++ (void)qmuimc_handleMenuWillShow { + UIWindow *window = [UIMenuController qmuimc_menuControllerWindow]; + UIWindow *targetWindow = [UIMenuController qmuimc_firstResponderWindowExceptMainWindow]; + if (window && targetWindow && ![QMUIHelper isKeyboardVisible]) { + QMUILog(@"UIMenuController", @"show menu - cur window level = %@, origin window level = %@ target window level = %@", @(window.windowLevel), @([window qmui_getBoundLongForKey:@"kOriginalWindowLevel"]), @(targetWindow.windowLevel)); + [window qmui_bindLong:window.windowLevel forKey:@"kOriginalWindowLevel"]; + [window qmui_bindBOOL:YES forKey:@"kWindowLevelChanged"]; + window.windowLevel = targetWindow.windowLevel + 1; + } +} + ++ (void)qmuimc_handleMenuWillHide { + UIWindow *window = [UIMenuController qmuimc_menuControllerWindow]; + if (window && [window qmui_getBoundBOOLForKey:@"kWindowLevelChanged"]) { + QMUILog(@"UIMenuController", @"hide menu - cur window level = %@, origin window level = %@", @(window.windowLevel), @([window qmui_getBoundLongForKey:@"kOriginalWindowLevel"])); + window.windowLevel = [window qmui_getBoundLongForKey:@"kOriginalWindowLevel"]; + [window qmui_bindLong:0 forKey:@"kOriginalWindowLevel"]; + [window qmui_bindBOOL:NO forKey:@"kWindowLevelChanged"]; + } +} + ++ (UIWindow *)qmuimc_menuControllerWindow { + if (kMenuControllerWindow && !kMenuControllerWindow.hidden) { + return kMenuControllerWindow; + } + [UIApplication.sharedApplication.windows enumerateObjectsUsingBlock:^(__kindof UIWindow * _Nonnull window, NSUInteger idx, BOOL * _Nonnull stop) { + NSString *windowString = [NSString stringWithFormat:@"UI%@%@", @"Text", @"EffectsWindow"]; + if ([window isKindOfClass:NSClassFromString(windowString)] && !window.hidden) { + if (@available(iOS 16.0, *)) { + UIView *view = [window.subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull item) { + return [NSStringFromClass(item.class) isEqualToString:[NSString qmui_stringByConcat:@"_", @"UI", @"EditMenu", @"ContainerView", nil]]; + }]; + if (view) { + kMenuControllerWindow = window; + } + } else { + [window.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) { + NSString *targetView = [NSString stringWithFormat:@"UI%@%@", @"Callout", @"Bar"]; + if ([subview isKindOfClass:NSClassFromString(targetView)]) { + kMenuControllerWindow = window; + *stop = YES; + } + }]; + } + } + }]; + return kMenuControllerWindow; +} + ++ (UIWindow *)qmuimc_firstResponderWindowExceptMainWindow { + __block UIWindow *resultWindow = nil; + [UIApplication.sharedApplication.windows enumerateObjectsUsingBlock:^(__kindof UIWindow * _Nonnull window, NSUInteger idx, BOOL * _Nonnull stop) { + if (window != UIApplication.sharedApplication.delegate.window) { + UIResponder *responder = [UIMenuController qmuimc_findFirstResponderInView:window]; + if (responder) { + resultWindow = window; + *stop = YES; + } + } + }]; + return resultWindow; +} + ++ (UIResponder *)qmuimc_findFirstResponderInView:(UIView *)view { + if (view.isFirstResponder) { + return view; + } + for (UIView *subView in view.subviews) { + id responder = [UIMenuController qmuimc_findFirstResponderInView:subView]; + if (responder) { + return responder; + } + } + return nil; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.h index 381189d1..7226ec6b 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UINavigationBar+QMUI.h @@ -20,29 +20,9 @@ NS_ASSUME_NONNULL_BEGIN @interface UINavigationBar (QMUI) /** - UINavigationBar 的背景 view,可能显示磨砂、背景图,顶部有一部分溢出到 UINavigationBar 外。 - - 在 iOS 10 及以后是私有的 _UIBarBackground 类。 - - 在 iOS 9 及以前是私有的 _UINavigationBarBackground 类。 - */ -@property(nonatomic, strong, readonly) UIView *qmui_backgroundView; - -/** - qmui_backgroundView 内显示实际背景的 view,可能是磨砂或者背景图片。 - - 在 iOS 10 及以后,该 view 为 qmui_backgroundView 的 subview,当显示磨砂时是一个 UIVisualEffectView,当显示背景图时是一个 UIImageView。 - - 在 iOS 9 及以前,如果显示磨砂,该 view 为 qmui_backgroundView 的 subview,是一个 _UIBackdropView,如果显示背景图,则返回 qmui_backgroundView 自身,因为 _UINavigationBarBackground 本身就是一个 UIImageView。 - - @warning 如果要以 view 的方式去修改 UINavigationBar 的背景,由于不同的 iOS 版本,qmui_shadowImageView 和 qmui_backgroundContentView 的层级关系不同,所以为了效果的统一,建议这种情况下操作 qmui_backgroundView 会好过于操作 qmui_backgroundContentView。 - */ -@property(nonatomic, strong, readonly) __kindof UIView *qmui_backgroundContentView; - -/** - qmui_backgroundView 内的 subview,用于显示底部分隔线 shadowImage,注意这个 view 是溢出到 qmui_backgroundView 外的。若 shadowImage 为 [UIImage new],则这个 view 的高度为 0。 + UINavigationBar 在 iOS 11 下所有的 item 都会由 contentView 管理,只要在 UINavigationController init 完成后就能拿到 qmui_contentView 的值 */ -@property(nonatomic, strong, readonly) UIImageView *qmui_shadowImageView; +@property(nonatomic, strong, readonly, nullable) UIView *qmui_contentView; @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m index c577061c..8d82a777 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UINavigationBar+QMUI.m @@ -14,30 +14,364 @@ // #import "UINavigationBar+QMUI.h" +#import "QMUICore.h" +#import "NSObject+QMUI.h" +#import "UIView+QMUI.h" +#import "NSArray+QMUI.h" +#import "UINavigationItem+QMUI.h" + +NSString *const kShouldFixTitleViewBugKey = @"kShouldFixTitleViewBugKey"; @implementation UINavigationBar (QMUI) -- (UIView *)qmui_backgroundView { - return [self valueForKey:@"_backgroundView"]; ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // [UIKit Bug] Xcode 14 编译的 App 在 iOS 16.0 上可能存在顶部标题布局错乱 + // https://github.com/Tencent/QMUI_iOS/issues/1457 +//#ifdef IOS16_SDK_ALLOWED 有些机子在 Xcode 13 编译的包上也有问题,所以先不做 Xcode 版本判断 + if (@available(iOS 16.0, *)) { + + if (@available(iOS 16.1, *)) { + // iOS 16.1 系统已修复 + } else { + OverrideImplementation([UINavigationItem class], @selector(setTitleView:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationItem *selfObject, UIView *firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, UIView *); + originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if (!firstArgv) return; + + UINavigationBar *navigationBar = selfObject.qmui_navigationBar; + [navigationBar qmuinb_fixTitleViewLayoutInIOS16]; + }; + }); + + OverrideImplementation([UINavigationBar class], @selector(pushNavigationItem:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UINavigationItem *navigationItem, BOOL animated) { + + if (!animated && !selfObject.topItem.titleView && navigationItem.titleView) { + [selfObject qmuinb_fixTitleViewLayoutInIOS16]; + } + + // call super + void (*originSelectorIMP)(id, SEL, UINavigationItem *, BOOL); + originSelectorIMP = (void (*)(id, SEL, UINavigationItem *, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, navigationItem, animated); + }; + }); + + OverrideImplementation([UINavigationBar class], @selector(setItems:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, NSArray *items, BOOL animated) { + + if (!animated && !selfObject.topItem.titleView && items.lastObject.titleView) { + [selfObject qmuinb_fixTitleViewLayoutInIOS16]; + } + + // call super + void (*originSelectorIMP)(id, SEL, NSArray *, BOOL); + originSelectorIMP = (void (*)(id, SEL, NSArray *, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, items, animated); + }; + }); + } + } +//#endif + + // [UIKit Bug] iOS 12 及以上的系统,如果设置了自己的 leftBarButtonItem,且 title 很长时,则当 pop 的时候,title 会瞬间跳到左边,与 leftBarButtonItem 重叠 + // https://github.com/Tencent/QMUI_iOS/issues/1217 + // _UITAMICAdaptorView + Class adaptorClass = NSClassFromString([NSString qmui_stringByConcat:@"_", @"UITAMIC", @"Adaptor", @"View", nil]); + + // -[_UINavigationBarContentView didAddSubview:] + OverrideImplementation(NSClassFromString([NSString qmui_stringByConcat:@"_", @"UINavigationBar", @"ContentView", nil]), @selector(didAddSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, UIView *firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, UIView *); + originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if ([firstArgv isKindOfClass:adaptorClass] || [firstArgv isKindOfClass:UILabel.class]) { + firstArgv.qmui_frameWillChangeBlock = ^CGRect(__kindof UIView * _Nonnull view, CGRect followingFrame) { + if ([view qmui_getBoundObjectForKey:kShouldFixTitleViewBugKey]) { + followingFrame = [[view qmui_getBoundObjectForKey:kShouldFixTitleViewBugKey] CGRectValue]; + } + return followingFrame; + }; + } + }; + }); + + void (^boundTitleViewMinXBlock)(UINavigationBar *, BOOL) = ^void(UINavigationBar *navigationBar, BOOL cleanup) { + + if (!navigationBar.topItem.leftBarButtonItem) return; + + UIView *titleView = nil; + UIView *adapterView = navigationBar.topItem.titleView.superview; + if ([adapterView isKindOfClass:adaptorClass]) { + titleView = adapterView; + } else { + titleView = [navigationBar.qmui_contentView.subviews qmui_filterWithBlock:^BOOL(__kindof UIView * _Nonnull item) { + return [item isKindOfClass:UILabel.class]; + }].firstObject; + } + if (!titleView) return; + + if (cleanup) { + [titleView qmui_bindObject:nil forKey:kShouldFixTitleViewBugKey]; + } else if (CGRectGetWidth(titleView.frame) > CGRectGetWidth(navigationBar.bounds) / 2) { + [titleView qmui_bindObject:[NSValue valueWithCGRect:titleView.frame] forKey:kShouldFixTitleViewBugKey]; + } + }; + + // // - [UINavigationBar _popNavigationItemWithTransition:] + // - (id) _popNavigationItemWithTransition:(int)arg1; (0x1a15513a0) + OverrideImplementation([UINavigationBar class], NSSelectorFromString([NSString qmui_stringByConcat:@"_", @"popNavigationItem", @"With", @"Transition:", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^id(UINavigationBar *selfObject, NSInteger firstArgv) { + + boundTitleViewMinXBlock(selfObject, NO); + + // call super + id (*originSelectorIMP)(id, SEL, NSInteger); + originSelectorIMP = (id (*)(id, SEL, NSInteger))originalIMPProvider(); + id result = originSelectorIMP(selfObject, originCMD, firstArgv); + return result; + }; + }); + + // - (void) _completePopOperationAnimated:(BOOL)arg1 transitionAssistant:(id)arg2; (0x1a1551668) + OverrideImplementation([UINavigationBar class], NSSelectorFromString([NSString qmui_stringByConcat:@"_", @"complete", @"PopOperationAnimated:", @"transitionAssistant:", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, BOOL firstArgv, id secondArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL, id); + originSelectorIMP = (void (*)(id, SEL, BOOL, id))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); + + boundTitleViewMinXBlock(selfObject, YES); + }; + }); + + // 以下是将 iOS 12 修改 UINavigationBar 样式的接口转换成用 iOS 13 的新接口去设置(因为新旧方法是互斥的,所以统一在新系统都用新方法) + // 虽然系统的新接口是 iOS 13 就已经存在,但由于 iOS 13、14 都没必要用新接口,所以 QMUI 里在 iOS 15 才开始使用新接口,所以下方的 @available 填的是 iOS 15 而非 iOS 13(与 QMUIConfiguration.m 对应)。 + // 但这样有个风险,因为 QMUIConfiguration 配置表里都是用 appearance 的方式去设置 standardAppearance,所以如果在 UINavigationBar 实例被添加到 window 之前修改过旧版任意一个样式接口,就会导致一个新的 UINavigationBarAppearance 对象被设置给 standardAppearance 属性,这样系统就会认为你这个 UINavigationBar 实例自定义了 standardAppearance,那么当它被 moveToWindow 时就不会自动应用 appearance 的值了,因此需要保证在添加到 window 前不要自行修改属性 +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + + void (^syncAppearance)(UINavigationBar *, void(^barActionBlock)(UINavigationBarAppearance *appearance)) = ^void(UINavigationBar *navigationBar, void(^barActionBlock)(UINavigationBarAppearance *appearance)) { + if (!barActionBlock) return; + + // 需要确保这里获取到的 navigationBar.standardAppearance 是已经被应用了 UIAppearance 全局样式后的值,否则会出现下方 issue 描述的问题 + // https://github.com/Tencent/QMUI_iOS/issues/1437 + UINavigationBarAppearance *appearance = navigationBar.standardAppearance; + barActionBlock(appearance); + navigationBar.standardAppearance = appearance; + if (QMUICMIActivated && NavBarUsesStandardAppearanceOnly) { + navigationBar.scrollEdgeAppearance = appearance; + } + }; + + 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); + + syncAppearance(selfObject, ^void(UINavigationBarAppearance *appearance) { + appearance.backgroundColor = barTintColor; + }); + }; + }); + + OverrideImplementation([UINavigationBar class], @selector(barTintColor), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIColor *(UINavigationBar *selfObject) { + return selfObject.standardAppearance.backgroundColor; + }; + }); + + OverrideImplementation([UINavigationBar class], @selector(setBackgroundImage:forBarPosition:barMetrics:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UIImage *image, UIBarPosition barPosition, UIBarMetrics barMetrics) { + + // call super + void (*originSelectorIMP)(id, SEL, UIImage *, UIBarPosition, UIBarMetrics); + originSelectorIMP = (void (*)(id, SEL, UIImage *, UIBarPosition, UIBarMetrics))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, image, barPosition, barMetrics); + + syncAppearance(selfObject, ^void(UINavigationBarAppearance *appearance) { + appearance.backgroundImage = image; + }); + }; + }); + + OverrideImplementation([UINavigationBar class], @selector(backgroundImageForBarPosition:barMetrics:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UINavigationBar *selfObject, UIBarPosition firstArgv, UIBarMetrics secondArgv) { + return selfObject.standardAppearance.backgroundImage; + }; + }); + + 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); + + syncAppearance(selfObject, ^void(UINavigationBarAppearance *appearance) { + appearance.shadowImage = shadowImage; + }); + }; + }); + + OverrideImplementation([UINavigationBar class], @selector(shadowImage), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UINavigationBar *selfObject) { + return selfObject.standardAppearance.shadowImage; + }; + }); + + 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); + + syncAppearance(selfObject, ^void(UINavigationBarAppearance *appearance) { + appearance.backgroundEffect = [UIBlurEffect effectWithStyle:barStyle == UIBarStyleDefault ? UIBlurEffectStyleSystemChromeMaterialLight : UIBlurEffectStyleSystemChromeMaterialDark]; + }); + }; + }); + + // iOS 15 没有对应的属性 +// OverrideImplementation([UINavigationBar class], @selector(barStyle), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { +// return ^UIBarStyle(UINavigationBar *selfObject) { +// +// if (@available(iOS 15.0, *)) { +// return ???; +// } +// +// +// // call super +// UIBarStyle (*originSelectorIMP)(id, SEL); +// originSelectorIMP = (UIBarStyle (*)(id, SEL))originalIMPProvider(); +// UIBarStyle result = originSelectorIMP(selfObject, originCMD); +// +// return result; +// }; +// }); + + OverrideImplementation([UINavigationBar class], @selector(setTitleTextAttributes:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, NSDictionary *titleTextAttributes) { + + // call super + void (*originSelectorIMP)(id, SEL, NSDictionary *); + originSelectorIMP = (void (*)(id, SEL, NSDictionary *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, titleTextAttributes); + + syncAppearance(selfObject, ^void(UINavigationBarAppearance *appearance) { + appearance.titleTextAttributes = titleTextAttributes; + }); + }; + }); + } + + if (@available(iOS 15.0, *)) { + if (!QMUICMIActivated) return; + if (!(NavBarRemoveBackgroundEffectAutomatically || TabBarRemoveBackgroundEffectAutomatically || ToolBarRemoveBackgroundEffectAutomatically) + && !(NavBarUsesStandardAppearanceOnly || TabBarUsesStandardAppearanceOnly || ToolBarUsesStandardAppearanceOnly)) return; + + // - [_UIBarBackground updateBackground] + OverrideImplementation(NSClassFromString(@"_UIBarBackground"), NSSelectorFromString(@"updateBackground"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject) { + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + if (!selfObject.superview) return; + if (!NavBarRemoveBackgroundEffectAutomatically && !NavBarUsesStandardAppearanceOnly && [selfObject.superview isKindOfClass:UINavigationBar.class]) return; + if (!TabBarRemoveBackgroundEffectAutomatically && !TabBarUsesStandardAppearanceOnly && [selfObject.superview isKindOfClass:UITabBar.class]) return; + if (!ToolBarRemoveBackgroundEffectAutomatically && !ToolBarUsesStandardAppearanceOnly && [selfObject.superview isKindOfClass:UIToolbar.class]) return; + + UIImageView *backgroundImageView1 = [selfObject valueForKey:@"_colorAndImageView1"]; + UIImageView *backgroundImageView2 = [selfObject valueForKey:@"_colorAndImageView2"]; + UIVisualEffectView *backgroundEffectView1 = [selfObject valueForKey:@"_effectView1"]; + UIVisualEffectView *backgroundEffectView2 = [selfObject valueForKey:@"_effectView2"]; + + // iOS 14 系统默认特性是存在 backgroundImage 则不存在其他任何背景,但如果存在 barTintColor 则磨砂 view 也可以共存。 + // iOS 15 系统默认特性是 backgroundImage、backgroundColor、backgroundEffect 三者都可以共存,其中前两者共用 _colorAndImageView,而我们这个开关为了符合 iOS 14 的特性,仅针对 _colorAndImageView 是因为 backgroundImage 存在而出现的情况做处理。 + if (NavBarRemoveBackgroundEffectAutomatically || TabBarRemoveBackgroundEffectAutomatically || ToolBarRemoveBackgroundEffectAutomatically) { + BOOL hasBackgroundImage1 = backgroundImageView1 && backgroundImageView1.superview && !backgroundImageView1.hidden && backgroundImageView1.image; + BOOL hasBackgroundImage2 = backgroundImageView2 && backgroundImageView2.superview && !backgroundImageView2.hidden && backgroundImageView2.image; + BOOL shouldHideEffectView = hasBackgroundImage1 || hasBackgroundImage2; + if (shouldHideEffectView) { + backgroundEffectView1.hidden = YES; + backgroundEffectView2.hidden = YES; + } else { + // 把 backgroundImage 置为 nil,理应要恢复 effectView 的显示,但由于 iOS 15 里 effectView 有2个,什么时候显示哪个取决于 contentScrollView 的滚动位置,而这个位置在当前上下文里我们是无法得知的,所以先不处理了,交给系统在下一次 updateBackground 时刷新吧... + } + } + + // 虽然 4.4.0 增加的这些开关会保证 scrollEdgeAppearance 也被设置,但系统始终都会同时显示两份 view(一份 standard 的一份 scrollEdge 的),当你的样式是不透明时没问题,但如果存在半透明,同时显示两份 view 就会导致两个半透明的效果重叠在一起,最终肉眼看到的样式和预期是不符合的,所以 4.4.4 开始,我们会强制让其中一份 view 隐藏掉。 + if (NavBarUsesStandardAppearanceOnly || TabBarUsesStandardAppearanceOnly || ToolBarUsesStandardAppearanceOnly) { + backgroundImageView2.hidden = YES; + backgroundEffectView2.hidden = YES; + } + }; + }); + + // 尚未应用 UIAppearance 就已经修改 bar 的样式的场景,可能导致 bar 样式无法与全局保持一致,所以这里做个提醒 + // https://github.com/Tencent/QMUI_iOS/issues/1451 + // - [UINavigationBar setStandardAppearance:] + OverrideImplementation([UINavigationBar class], @selector(setStandardAppearance:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UINavigationBarAppearance * firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, UINavigationBarAppearance *); + originSelectorIMP = (void (*)(id, SEL, UINavigationBarAppearance *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + // 这里只希望识别 UINavigationController 自带的 navigationBar,不希望处理业务自己 new 的 bar,所以用 superview 是否为 UILayoutContainerView 来作为判断条件。 + 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 的样式无法与全局保持一致。"); + } + }; + }); + } +#endif + }); } -- (__kindof UIView *)qmui_backgroundContentView { - if (@available(iOS 10, *)) { - UIImageView *imageView = [self.qmui_backgroundView valueForKey:@"_backgroundImageView"]; - UIVisualEffectView *visualEffectView = [self.qmui_backgroundView valueForKey:@"_backgroundEffectView"]; - UIView *customView = [self.qmui_backgroundView valueForKey:@"_customBackgroundView"]; - UIView *result = customView && customView.superview ? customView : (imageView && imageView.superview ? imageView : visualEffectView); - return result; - } else { - UIView *backdrop = [self.qmui_backgroundView valueForKey:@"_adaptiveBackdrop"]; - UIView *result = backdrop && backdrop.superview ? backdrop : self.qmui_backgroundView; - return result; - } +- (UIView *)qmui_contentView { + return [self valueForKeyPath:@"visualProvider.contentView"]; } -- (UIImageView *)qmui_shadowImageView { - // UINavigationBar 在 init 完就可以获取到 backgroundView 和 shadowView,无需关心调用时机的问题 - return [self.qmui_backgroundView valueForKey:@"_shadowView"]; +- (void)qmuinb_fixTitleViewLayoutInIOS16 { + // _UINavigationBarTitleControl,在每次转场动画时都会被重建,但无动画则一直都是这个实例(横竖屏切换也是同一个实例) + Class titleControlClass = NSClassFromString([NSString qmui_stringByConcat:@"_", @"UINavigationBar", @"TitleControl", nil]); + UIView *titleControl = [self.qmui_contentView.subviews qmui_filterWithBlock:^BOOL(__kindof UIView * _Nonnull item) { + return [item isKindOfClass:titleControlClass]; + }].firstObject; + titleControl.qmui_frameWillChangeBlock = ^CGRect(__kindof UIView * _Nonnull view, CGRect followingFrame) { + followingFrame = CGRectSetY(followingFrame, CGRectGetMinYVerticallyCenterInParentRect(view.superview.bounds, followingFrame)); + return followingFrame; + }; } @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h index d3e943fe..1a80c2d0 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UINavigationController+QMUI.h @@ -18,10 +18,66 @@ NS_ASSUME_NONNULL_BEGIN +typedef NS_ENUM(NSUInteger, QMUINavigationAction) { + QMUINavigationActionUnknow, // 初始、各种动作的 completed 之后都会立即转入 unknown 状态,此时的 appearing、disappearingViewController 均为 nil + + QMUINavigationActionWillPush, // push 方法被触发,但尚未进行真正的 push 动作 + QMUINavigationActionDidPush, // 系统的 push 已经执行完,viewControllers 已被刷新 + QMUINavigationActionPushCompleted, // push 动画结束(如果没有动画,则在 did push 后立即进入 completed) + + QMUINavigationActionWillPop, // pop 方法被触发,但尚未进行真正的 pop 动作 + QMUINavigationActionDidPop, // 系统的 pop 已经执行完,viewControllers 已被刷新(注意可能有 pop 失败的情况) + QMUINavigationActionPopCompleted, // pop 动画结束(如果没有动画,则在 did pop 后立即进入 completed) + + QMUINavigationActionWillSet, // setViewControllers 方法被触发,但尚未进行真正的 set 动作 + QMUINavigationActionDidSet, // 系统的 setViewControllers 已经执行完,viewControllers 已被刷新 + QMUINavigationActionSetCompleted, // setViewControllers 动画结束(如果没有动画,则在 did set 后立即进入 completed) +}; + +typedef void (^QMUINavigationActionDidChangeBlock)(QMUINavigationAction action, BOOL animated, __kindof UINavigationController * _Nullable weakNavigationController, __kindof UIViewController * _Nullable appearingViewController, NSArray<__kindof UIViewController *> * _Nullable disappearingViewControllers); + + @interface UINavigationController (QMUI) +/** + NS_DESIGNATED_INITIALIZER 方法被调用时就会调用这个方法,一些 init 时要处理的事情都可以统一放在这里面。 + 为什么需要创建这个方法,是因为 UINavigationController 的 NS_DESIGNATED_INITIALIZER 数量太多了有4个,而且 iOS 12 及以前,initWithNavigationBarClass:toolbarClass:、initWithRootViewController: 这2个方法是没被标记为 NS_DESIGNATED_INITIALIZER 的,它们都会调用 initWithNibName:bundle:,但 iOS 13 及以后,这两个方法增加了 NS_DESIGNATED_INITIALIZER 标记。 + 由于有 iOS 版本差异,业务也需要做版本判断,才能保证 init 逻辑不会被重复调用,于是 QMUI 直接提供这个方法,省去业务的判断。 + */ +- (void)qmui_didInitialize NS_REQUIRES_SUPER; + +@property(nonatomic, assign, readonly) QMUINavigationAction qmui_navigationAction; + +/** + 添加一个 block 用于监听当前 UINavigationController 的 push/pop/setViewControllers 操作,在即将进行、已经进行、动画已完结等各种状态均会回调。 + block 参数里的 appearingViewController 表示即将显示的界面。 + disappearingViewControllers 表示即将消失的界面,数组形式是因为可能一次性 pop 掉多个(例如 popToRootViewController、setViewControllers),此时只有 disappearingViewControllers.lastObject 可以看到 pop 动画。由于 pop 可能失败,所以 will 动作里的 disappearingViewControllers 最终不一定真的会被移除。 + weakNavigationController 是便于你引用 self 而避免循环引用(因为这个方法会令 self retain 你传进来的 block,而 block 内部如果直接用 self,就会 retain self,产生循环引用,所以这里给一个参数规避这个问题)。 + @note 无法添加一个只监听某个 QMUINavigationAction 的 block,每一个添加的 block 在任何一个 action 变化时都会被调用,需要 block 内部自己区分当前的 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; + /// 获取rootViewController -- (nullable UIViewController *)qmui_rootViewController; +@property(nullable, nonatomic, readonly) UIViewController *qmui_rootViewController; + +/// QMUI 会修改 UINavigationController.interactivePopGestureRecognizer.delegate 的值,因此提供一个属性用于获取系统原始的值 +@property(nullable, nonatomic, weak, readonly) id qmui_interactivePopGestureRecognizerDelegate; - (void)qmui_pushViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^_Nullable)(void))completion; - (UIViewController *)qmui_popViewControllerAnimated:(BOOL)animated completion:(void (^_Nullable)(void))completion; @@ -39,11 +95,11 @@ NS_ASSUME_NONNULL_BEGIN @optional -/// 是否需要拦截系统返回按钮的事件,只有当这里返回YES的时候,才会询问方法:`canPopViewController` -- (BOOL)shouldHoldBackButtonEvent; - -/// 是否可以`popViewController`,可以在这个返回里面做一些业务的判断,比如点击返回按钮的时候,如果输入框里面的文本没有满足条件的则可以弹alert并且返回NO -- (BOOL)canPopViewController; +/** + * 点击系统返回按钮或者手势返回的时候是否要相应界面返回(手动调用代码pop排除)。支持参数判断是点击系统返回按钮还是通过手势触发 + * 一般使用的场景是:可以在这个返回里面做一些业务的判断,比如点击返回按钮的时候,如果输入框里面的文本没有满足条件的则可以弹 Alert 并且返回 NO 来阻止用户退出界面导致不合法的数据或者数据丢失。 + */ +- (BOOL)shouldPopViewControllerByBackButtonOrPopGesture:(BOOL)byPopGesture; /// 当自定义了`leftBarButtonItem`按钮之后,系统的手势返回就失效了。可以通过`forceEnableInteractivePopGestureRecognizer`来决定要不要把那个手势返回强制加回来。当 interactivePopGestureRecognizer.enabled = NO 或者当前`UINavigationController`堆栈的viewControllers小于2的时候此方法无效。 - (BOOL)forceEnableInteractivePopGestureRecognizer; diff --git a/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m index 723cfb85..f5a15064 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UINavigationController+QMUI.m @@ -16,144 +16,592 @@ #import "UINavigationController+QMUI.h" #import "QMUICore.h" #import "QMUILog.h" -#import "QMUIWeakObjectContainer.h" +#import "UIViewController+QMUI.h" -@interface UINavigationController (BackButtonHandlerProtocol) - -// `UINavigationControllerBackButtonHandlerProtocol`的`canPopViewController`功能里面,当 A canPop = NO,B canPop = YES,那么从 B 手势返回到 A,也会触发 A 的 `canPopViewController` 方法,这是因为手势返回会去询问`gestureRecognizerShouldBegin:`和`qmuinav_navigationBar:shouldPopItem:`,而这两个方法里面的 self.topViewController 是不同的对象,所以导致这个问题。所以通过 tmp_topViewController 来记录 self.topViewController 从而保证两个地方的值是相等的。 -// 手势从 B 返回 A,如果 A 没有 navBar,那么`qmuinav_navigationBar:shouldPopItem:`是不会被调用的,所以导致 tmp_topViewController 没有被释放,所以 tmp_topViewController 需要使用 weak 来修饰(https://github.com/Tencent/QMUI_iOS/issues/251) -@property(nonatomic, weak) UIViewController *tmp_topViewController; +@interface _QMUINavigationInteractiveGestureDelegator : NSObject +@property(nonatomic, weak, readonly) UINavigationController *parentViewController; +- (instancetype)initWithParentViewController:(UINavigationController *)parentViewController; @end -@implementation UINavigationController (BackButtonHandlerProtocol) - -QMUISynthesizeIdWeakProperty(tmp_topViewController, setTmp_topViewController) +@interface UINavigationController () +@property(nonatomic, strong) NSMutableArray *qmuinc_navigationActionDidChangeBlocks; +@property(nullable, nonatomic, readwrite) UIViewController *qmui_endedTransitionTopViewController; +@property(nullable, nonatomic, weak, readonly) id qmui_interactivePopGestureRecognizerDelegate; +@property(nullable, nonatomic, strong) _QMUINavigationInteractiveGestureDelegator *qmui_interactiveGestureDelegator; @end - @implementation UINavigationController (QMUI) +QMUISynthesizeBOOLProperty(qmui_alwaysInvokeAppearanceMethods, setQmui_alwaysInvokeAppearanceMethods) +QMUISynthesizeIdStrongProperty(qmuinc_navigationActionDidChangeBlocks, setQmuinc_navigationActionDidChangeBlocks) +QMUISynthesizeIdWeakProperty(qmui_endedTransitionTopViewController, setQmui_endedTransitionTopViewController) +QMUISynthesizeIdWeakProperty(qmui_interactivePopGestureRecognizerDelegate, setQmui_interactivePopGestureRecognizerDelegate) +QMUISynthesizeIdStrongProperty(qmui_interactiveGestureDelegator, setQmui_interactiveGestureDelegator) + + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ + OverrideImplementation([UINavigationController class], @selector(initWithNibName:bundle:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UINavigationController *(UINavigationController *selfObject, NSString *firstArgv, NSBundle *secondArgv) { + + // call super + UINavigationController *(*originSelectorIMP)(id, SEL, NSString *, NSBundle *); + originSelectorIMP = (UINavigationController *(*)(id, SEL, NSString *, NSBundle *))originalIMPProvider(); + UINavigationController *result = originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); + + [selfObject qmui_didInitialize]; + + return result; + }; + }); + + OverrideImplementation([UINavigationController class], @selector(initWithCoder:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UINavigationController *(UINavigationController *selfObject, NSCoder *firstArgv) { + + // call super + UINavigationController *(*originSelectorIMP)(id, SEL, NSCoder *); + originSelectorIMP = (UINavigationController *(*)(id, SEL, NSCoder *))originalIMPProvider(); + UINavigationController *result = originSelectorIMP(selfObject, originCMD, firstArgv); + + [selfObject qmui_didInitialize]; + + return result; + }; + }); + + // iOS 12 及以前,initWithNavigationBarClass:toolbarClass:、initWithRootViewController: 会调用 initWithNibName:bundle:,所以这两个方法在 iOS 12 下不需要再次调用 qmui_didInitialize 了。 + OverrideImplementation([UINavigationController class], @selector(initWithNavigationBarClass:toolbarClass:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UINavigationController *(UINavigationController *selfObject, Class firstArgv, Class secondArgv) { + + // call super + UINavigationController *(*originSelectorIMP)(id, SEL, Class, Class); + originSelectorIMP = (UINavigationController *(*)(id, SEL, Class, Class))originalIMPProvider(); + UINavigationController *result = originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); + + [selfObject qmui_didInitialize]; + + return result; + }; + }); + + OverrideImplementation([UINavigationController class], @selector(initWithRootViewController:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UINavigationController *(UINavigationController *selfObject, UIViewController *firstArgv) { + + // call super + UINavigationController *(*originSelectorIMP)(id, SEL, UIViewController *); + originSelectorIMP = (UINavigationController *(*)(id, SEL, UIViewController *))originalIMPProvider(); + UINavigationController *result = originSelectorIMP(selfObject, originCMD, firstArgv); + + [selfObject qmui_didInitialize]; + + return result; + }; + }); + + ExtendImplementationOfVoidMethodWithoutArguments([UINavigationController class], @selector(viewDidLoad), ^(UINavigationController *selfObject) { - objc_setAssociatedObject(selfObject, &originGestureDelegateKey, selfObject.interactivePopGestureRecognizer.delegate, OBJC_ASSOCIATION_ASSIGN); - selfObject.interactivePopGestureRecognizer.delegate = (id)selfObject; + selfObject.qmui_interactivePopGestureRecognizerDelegate = selfObject.interactivePopGestureRecognizer.delegate; + selfObject.qmui_interactiveGestureDelegator = [[_QMUINavigationInteractiveGestureDelegator alloc] initWithParentViewController:selfObject]; + selfObject.interactivePopGestureRecognizer.delegate = selfObject.qmui_interactiveGestureDelegator; + + // 根据 NavBarContainerClasses 的值来决定是否应用 bar.tintColor + // tintColor 没有被添加 UI_APPEARANCE_SELECTOR,所以没有采用 UIAppearance 的方式去实现(虽然它实际上是支持的) + if (QMUICMIActivated) { + BOOL shouldSetTintColor = NO; + if (NavBarContainerClasses.count) { + for (Class class in NavBarContainerClasses) { + if ([selfObject isKindOfClass:class]) { + shouldSetTintColor = YES; + break; + } + } + } else { + shouldSetTintColor = YES; + } + if (shouldSetTintColor) { + selfObject.navigationBar.tintColor = NavBarTintColor; + } + } + if (QMUICMIActivated) { + BOOL shouldSetTintColor = NO; + if (ToolBarContainerClasses.count) { + for (Class class in ToolBarContainerClasses) { + if ([selfObject isKindOfClass:class]) { + shouldSetTintColor = YES; + break; + } + } + } else { + shouldSetTintColor = YES; + } + if (shouldSetTintColor) { + selfObject.toolbar.tintColor = ToolBarTintColor; + } + } }); - OverrideImplementation([UINavigationController class], @selector(navigationBar:shouldPopItem:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^BOOL(UINavigationController *selfObject, UINavigationBar *navigationBar, UINavigationItem *item) { + OverrideImplementation(NSClassFromString([NSString qmui_stringByConcat:@"_", @"UINavigationBar", @"ContentView", nil]), NSSelectorFromString(@"__backButtonAction:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, id firstArgv) { + + if ([selfObject.superview isKindOfClass:UINavigationBar.class]) { + UINavigationBar *bar = (UINavigationBar *)selfObject.superview; + if ([bar.delegate isKindOfClass:UINavigationController.class]) { + UINavigationController *navController = (UINavigationController *)bar.delegate; + BOOL canPopViewController = [navController canPopViewController:navController.topViewController byPopGesture:NO]; + if (!canPopViewController) return; + } + } // call super - BOOL (^callSuperBlock)(UINavigationController *, UINavigationBar *, UINavigationItem *) = ^BOOL(UINavigationController *aSelfObject, UINavigationBar *aNavigationBar, UINavigationItem *aItem) { - BOOL (*originSelectorIMP)(id, SEL, UINavigationBar *, UINavigationItem *); - originSelectorIMP = (BOOL (*)(id, SEL, UINavigationBar *, UINavigationItem *))originalIMPProvider(); - BOOL result = originSelectorIMP(aSelfObject, originCMD, aNavigationBar, aItem); - return result; + void (*originSelectorIMP)(id, SEL, id); + originSelectorIMP = (void (*)(id, SEL, id))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + }; + }); + + OverrideImplementation([UINavigationController class], NSSelectorFromString(@"navigationTransitionView:didEndTransition:fromView:toView:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^void(UINavigationController *selfObject, UIView *transitionView, NSInteger transition, UIView *fromView, UIView *toView) { + + BOOL (*originSelectorIMP)(id, SEL, UIView *, NSInteger , UIView *, UIView *); + originSelectorIMP = (BOOL (*)(id, SEL, UIView *, NSInteger , UIView *, UIView *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, transitionView, transition, fromView, toView); + selfObject.qmui_endedTransitionTopViewController = selfObject.topViewController; + }; + }); + +#pragma mark - pushViewController:animated: + OverrideImplementation([UINavigationController class], @selector(pushViewController:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationController *selfObject, UIViewController *viewController, BOOL animated) { + + 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]) { + QMUIAssert(NO, @"UINavigationController (QMUI)", @"不允许重复 push 相同的 viewController 实例,会产生 crash。当前 viewController:%@", viewController); + return; + } + + // call super + void (^callSuperBlock)(void) = ^void(void) { + void (*originSelectorIMP)(id, SEL, UIViewController *, BOOL); + originSelectorIMP = (void (*)(id, SEL, UIViewController *, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, viewController, animated); }; - // avoid superclass - if (![selfObject isKindOfClass:originClass]) return callSuperBlock(selfObject, navigationBar, item); + BOOL willPushActually = viewController && ![selfObject.viewControllers containsObject:viewController]; + + if (!willPushActually) { + QMUIAssert(NO, @"UINavigationController (QMUI)", @"调用了 pushViewController 但实际上没 push 成功,viewController:%@", viewController); + callSuperBlock(); + return; + } + + UIViewController *appearingViewController = viewController; + NSArray *disappearingViewControllers = selfObject.topViewController ? @[selfObject.topViewController] : nil; - // 如果nav的vc栈中有两个vc,第一个是root,第二个是second。这时second页面如果点击系统的返回按钮,topViewController获取的栈顶vc是second,而如果是直接代码写的pop操作,则获取的栈顶vc是root。也就是说只要代码写了pop操作,则系统会直接将顶层vc也就是second出栈,然后才回调的,所以这时我们获取到的顶层vc就是root了。然而不管哪种方式,参数中的item都是second的item。 - BOOL isPopedByCoding = item != [selfObject topViewController].navigationItem; + [selfObject setQmui_navigationAction:QMUINavigationActionWillPush animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; - // !isPopedByCoding 要放在前面,这样当 !isPopedByCoding 不满足的时候就不会去询问 canPopViewController 了,可以避免额外调用 canPopViewController 里面的逻辑 - BOOL canPopViewController = !isPopedByCoding && [selfObject canPopViewController:selfObject.tmp_topViewController ?: [selfObject topViewController]]; + if (shouldInvokeAppearanceMethod) { + [disappearingViewControllers.lastObject beginAppearanceTransition:NO animated:animated]; + [appearingViewController beginAppearanceTransition:YES animated:animated]; + } + + callSuperBlock(); + + [selfObject setQmui_navigationAction:QMUINavigationActionDidPush animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + + [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]; + } + }]; + }; + }); + +#pragma mark - popViewControllerAnimated: + OverrideImplementation([UINavigationController class], @selector(popViewControllerAnimated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIViewController *(UINavigationController *selfObject, BOOL animated) { - if (canPopViewController || isPopedByCoding) { - selfObject.tmp_topViewController = nil; - BOOL result = callSuperBlock(selfObject, navigationBar, item); + // call super + UIViewController *(^callSuperBlock)(void) = ^UIViewController *(void) { + UIViewController *(*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (UIViewController *(*)(id, SEL, BOOL))originalIMPProvider(); + UIViewController *result = originSelectorIMP(selfObject, originCMD, animated); return result; + }; + + QMUINavigationAction action = selfObject.qmui_navigationAction; + if (action != QMUINavigationActionUnknow) { + QMUILogWarn(@"UINavigationController (QMUI)", @"popViewController 时上一次的转场尚未完成,系统会忽略本次 pop,等上一次转场完成后再重新执行 pop, viewControllers = %@", selfObject.viewControllers); + } + BOOL willPopActually = selfObject.viewControllers.count > 1 && action == QMUINavigationActionUnknow;// 系统文档里说 rootViewController 是不能被 pop 的,当只剩下 rootViewController 时当前方法什么事都不会做 + + if (!willPopActually) { + 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 + // https://github.com/Tencent/QMUI_iOS/issues/1180 + QMUIAssert(result && disappearingViewControllers && disappearingViewControllers.firstObject == result, @"UINavigationController (QMUI)", @"QMUI 认为 popViewController 会成功,但实际上失败了,result = %@, disappearingViewControllers = %@", result, disappearingViewControllers); + disappearingViewControllers = result ? @[result] : disappearingViewControllers; + + [selfObject setQmui_navigationAction:QMUINavigationActionDidPop animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + + 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 并不会被执行,所以这里改为手动调用 + if (transitionCompletion) { + transitionCompletion(); + } } else { - selfObject.tmp_topViewController = nil; - [selfObject resetSubviewsInNavBar:navigationBar]; + [selfObject qmui_animateAlongsideTransition:nil completion:^(id _Nonnull context) { + if (transitionCompletion) { + transitionCompletion(); + } + }]; } - return NO; + return result; + }; + }); + +#pragma mark - popToViewController:animated: + OverrideImplementation([UINavigationController class], @selector(popToViewController:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^NSArray *(UINavigationController *selfObject, UIViewController *viewController, BOOL animated) { + + // call super + NSArray *(^callSuperBlock)(void) = ^NSArray *(void) { + NSArray *(*originSelectorIMP)(id, SEL, UIViewController *, BOOL); + originSelectorIMP = (NSArray * (*)(id, SEL, UIViewController *, BOOL))originalIMPProvider(); + NSArray *poppedViewControllers = originSelectorIMP(selfObject, originCMD, viewController, animated); + return poppedViewControllers; + }; + + QMUINavigationAction action = selfObject.qmui_navigationAction; + if (action != QMUINavigationActionUnknow) { + QMUILogWarn(@"UINavigationController (QMUI)", @"popToViewController 时上一次的转场尚未完成,系统会忽略本次 pop,等上一次转场完成后再重新执行 pop, currentViewControllers = %@, viewController = %@", selfObject.viewControllers, viewController); + } + BOOL willPopActually = selfObject.viewControllers.count > 1 && [selfObject.viewControllers containsObject:viewController] && selfObject.topViewController != viewController && action == QMUINavigationActionUnknow;// 系统文档里说 rootViewController 是不能被 pop 的,当只剩下 rootViewController 时当前方法什么事都不会做 + + if (!willPopActually) { + return callSuperBlock(); + } + + UIViewController *appearingViewController = viewController; + NSArray *disappearingViewControllers = nil; + NSUInteger index = [selfObject.viewControllers indexOfObject:appearingViewController]; + if (index != NSNotFound) { + disappearingViewControllers = [selfObject.viewControllers subarrayWithRange:NSMakeRange(index + 1, selfObject.viewControllers.count - index - 1)]; + } + + [selfObject setQmui_navigationAction:QMUINavigationActionWillPop animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + + NSArray *result = callSuperBlock(); + + QMUIAssert(!(selfObject.isViewLoaded && selfObject.view.window) || [result isEqualToArray:disappearingViewControllers], @"UINavigationController (QMUI)", @"QMUI 计算得到的 popToViewController 结果和系统的不一致"); + disappearingViewControllers = result ?: disappearingViewControllers; + + [selfObject setQmui_navigationAction:QMUINavigationActionDidPop animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + + [selfObject qmui_animateAlongsideTransition:nil completion:^(id _Nonnull context) { + [selfObject setQmui_navigationAction:QMUINavigationActionPopCompleted animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + [selfObject setQmui_navigationAction:QMUINavigationActionUnknow animated:animated appearingViewController:nil disappearingViewControllers:nil]; + }]; + + return result; + }; + }); + +#pragma mark - popToRootViewControllerAnimated: + OverrideImplementation([UINavigationController class], @selector(popToRootViewControllerAnimated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^NSArray *(UINavigationController *selfObject, BOOL animated) { + + // call super + NSArray *(^callSuperBlock)(void) = ^NSArray *(void) { + NSArray *(*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (NSArray * (*)(id, SEL, BOOL))originalIMPProvider(); + NSArray *result = originSelectorIMP(selfObject, originCMD, animated); + return result; + }; + + QMUINavigationAction action = selfObject.qmui_navigationAction; + if (action != QMUINavigationActionUnknow) { + QMUILogWarn(@"UINavigationController (QMUI)", @"popToRootViewController 时上一次的转场尚未完成,系统会忽略本次 pop,等上一次转场完成后再重新执行 pop, viewControllers = %@", selfObject.viewControllers); + } + BOOL willPopActually = selfObject.viewControllers.count > 1 && action == QMUINavigationActionUnknow; + + if (!willPopActually) { + return callSuperBlock(); + } + + UIViewController *appearingViewController = selfObject.qmui_rootViewController; + NSArray *disappearingViewControllers = [selfObject.viewControllers subarrayWithRange:NSMakeRange(1, selfObject.viewControllers.count - 1)]; + + [selfObject setQmui_navigationAction:QMUINavigationActionWillPop animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + + NSArray *result = callSuperBlock(); + + // UINavigationController 不可见时 return 值可能为 nil + // https://github.com/Tencent/QMUI_iOS/issues/1180 + QMUIAssert(!(selfObject.isViewLoaded && selfObject.view.window) || [result isEqualToArray:disappearingViewControllers], @"UINavigationController (QMUI)", @"QMUI 计算得到的 popToRootViewController 结果和系统的不一致"); + disappearingViewControllers = result ?: disappearingViewControllers; + + [selfObject setQmui_navigationAction:QMUINavigationActionDidPop animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + + [selfObject qmui_animateAlongsideTransition:nil completion:^(id _Nonnull context) { + [selfObject setQmui_navigationAction:QMUINavigationActionPopCompleted animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + [selfObject setQmui_navigationAction:QMUINavigationActionUnknow animated:animated appearingViewController:nil disappearingViewControllers:nil]; + }]; + + return result; + }; + }); + +#pragma mark - setViewControllers:animated: + OverrideImplementation([UINavigationController class], @selector(setViewControllers:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationController *selfObject, NSArray *viewControllers, BOOL animated) { + + if (viewControllers.count != [NSSet setWithArray:viewControllers].count) { + QMUIAssert(NO, @"UINavigationController (QMUI)", @"setViewControllers 数组里不允许出现重复元素:%@", viewControllers); + viewControllers = [NSOrderedSet orderedSetWithArray:viewControllers].array;// 这里会保留该 vc 第一次出现的位置不变 + } + + UIViewController *appearingViewController = selfObject.topViewController != viewControllers.lastObject ? viewControllers.lastObject : nil;// setViewControllers 执行前后 topViewController 没有变化,则赋值为 nil,表示没有任何界面有“重新显示”,这个 nil 的值也用于在 QMUINavigationController 里实现 viewControllerKeepingAppearWhenSetViewControllersWithAnimated: + NSMutableArray *disappearingViewControllers = selfObject.viewControllers.mutableCopy; + [disappearingViewControllers removeObjectsInArray:viewControllers]; + disappearingViewControllers = disappearingViewControllers.count ? disappearingViewControllers : nil; + + [selfObject setQmui_navigationAction:QMUINavigationActionWillSet animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + + // call super + void (*originSelectorIMP)(id, SEL, NSArray *, BOOL); + originSelectorIMP = (void (*)(id, SEL, NSArray *, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, viewControllers, animated); + + [selfObject setQmui_navigationAction:QMUINavigationActionDidSet animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + + [selfObject qmui_animateAlongsideTransition:nil completion:^(id _Nonnull context) { + [selfObject setQmui_navigationAction:QMUINavigationActionSetCompleted animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + [selfObject setQmui_navigationAction:QMUINavigationActionUnknow animated:animated appearingViewController:nil disappearingViewControllers:nil]; + }]; }; }); }); } -static char originGestureDelegateKey; +- (void)qmui_didInitialize { +} + +static char kAssociatedObjectKey_navigationAction; +- (void)setQmui_navigationAction:(QMUINavigationAction)qmui_navigationAction + animated:(BOOL)animated + appearingViewController:(UIViewController *)appearingViewController + disappearingViewControllers:(NSArray *)disappearingViewControllers { + BOOL valueChanged = self.qmui_navigationAction != qmui_navigationAction; + objc_setAssociatedObject(self, &kAssociatedObjectKey_navigationAction, @(qmui_navigationAction), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (valueChanged && self.qmuinc_navigationActionDidChangeBlocks.count) { + [self.qmuinc_navigationActionDidChangeBlocks enumerateObjectsUsingBlock:^(QMUINavigationActionDidChangeBlock _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + obj(qmui_navigationAction, animated, self, appearingViewController, disappearingViewControllers); + }]; + } +} + +- (QMUINavigationAction)qmui_navigationAction { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_navigationAction)) unsignedIntegerValue]; +} + +- (void)qmui_addNavigationActionDidChangeBlock:(QMUINavigationActionDidChangeBlock)block { + if (!self.qmuinc_navigationActionDidChangeBlocks) { + self.qmuinc_navigationActionDidChangeBlocks = NSMutableArray.new; + } + [self.qmuinc_navigationActionDidChangeBlocks addObject:block]; +} + +- (BOOL)qmui_isPushing { + BOOL isPushing = self.qmui_navigationAction > QMUINavigationActionWillPush && self.qmui_navigationAction <= QMUINavigationActionPushCompleted; + return isPushing; +} + +- (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; + } + return self.qmui_endedTransitionTopViewController ? self.qmui_endedTransitionTopViewController : self.topViewController; +} - (nullable UIViewController *)qmui_rootViewController { - return self.viewControllers.firstObject; + UIViewController *rootViewController = self.viewControllers.firstObject; + // 系统 UINavigationController 的 popToViewController、popToRootViewController、setViewControllers 三种 pop 的方式都有一个共同的特点,假如此时有3个及以上的 vc 例如 [A,B,C],从当前界面 pop 到非相邻的界面,例如C到A,执行完 pop 操作后立马访问 UINavigationController.viewControllers 预期应该得到 [A],实际上会得到 [C,A],过一会(nav.view layoutIfNeeded 之后)才变成正确的 [A]。同理,[A,B,C,D]时从 D pop 到 B,预期得到[A,B],实际得到[D,A,B],也即这种情况它总是会把当前界面塞到 viewControllers 数组里的第一个,这就导致这期间访问基于 viewControllers 数组实现的功能(例如 qmui_rootViewController、qmui_previousViewController),都可能出错,所以这里对上述情况做特殊保护。 + // 如果 pop 操作时只有2个vc,则没这种问题。 + if (self.viewControllers.count > 1 && self.qmui_isPopping && self.transitionCoordinator) { + id transitionCoordinator = self.transitionCoordinator; + UIViewController *fromVc = [transitionCoordinator viewControllerForKey:UITransitionContextFromViewControllerKey]; + if (rootViewController == fromVc) { + rootViewController = self.viewControllers[1]; + } + } + return rootViewController; } - (void)qmui_pushViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^)(void))completion { - [QMUIHelper executeAnimationBlock:^{ - [self pushViewController:viewController animated:animated]; - } completionBlock:completion]; + // 要先进行转场操作才能产生 self.transitionCoordinator,然后才能用 qmui_animateAlongsideTransition:completion:,所以不能把转场操作放在 animation block 里。 + [self pushViewController:viewController animated:animated]; + if (completion) { + [self qmui_animateAlongsideTransition:nil completion:^(id _Nonnull context) { + completion(); + }]; + } } - (UIViewController *)qmui_popViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion { - __block UIViewController *result = nil; - [QMUIHelper executeAnimationBlock:^{ - result = [self popViewControllerAnimated:animated]; - } completionBlock:completion]; + // 要先进行转场操作才能产生 self.transitionCoordinator,然后才能用 qmui_animateAlongsideTransition:completion:,所以不能把转场操作放在 animation block 里。 + UIViewController *result = [self popViewControllerAnimated:animated]; + if (completion) { + [self qmui_animateAlongsideTransition:nil completion:^(id _Nonnull context) { + completion(); + }]; + } return result; } - (NSArray *)qmui_popToViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^)(void))completion { - __block NSArray *result = nil; - [QMUIHelper executeAnimationBlock:^{ - result = [self popToViewController:viewController animated:animated]; - } completionBlock:completion]; + // 要先进行转场操作才能产生 self.transitionCoordinator,然后才能用 qmui_animateAlongsideTransition:completion:,所以不能把转场操作放在 animation block 里。 + NSArray *result = [self popToViewController:viewController animated:animated]; + if (completion) { + [self qmui_animateAlongsideTransition:nil completion:^(id _Nonnull context) { + completion(); + }]; + } return result; } - (NSArray *)qmui_popToRootViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion { - __block NSArray *result = nil; - [QMUIHelper executeAnimationBlock:^{ - result = [self popToRootViewControllerAnimated:animated]; - } completionBlock:completion]; + // 要先进行转场操作才能产生 self.transitionCoordinator,然后才能用 qmui_animateAlongsideTransition:completion:,所以不能把转场操作放在 animation block 里。 + NSArray *result = [self popToRootViewControllerAnimated:animated]; + if (completion) { + [self qmui_animateAlongsideTransition:nil completion:^(id _Nonnull context) { + completion(); + }]; + } return result; } -- (BOOL)canPopViewController:(UIViewController *)viewController { +- (BOOL)canPopViewController:(UIViewController *)viewController byPopGesture:(BOOL)byPopGesture { BOOL canPopViewController = YES; - if ([viewController respondsToSelector:@selector(shouldHoldBackButtonEvent)] && - [viewController shouldHoldBackButtonEvent] && - [viewController respondsToSelector:@selector(canPopViewController)] && - ![viewController canPopViewController]) { + if ([viewController respondsToSelector:@selector(shouldPopViewControllerByBackButtonOrPopGesture:)] && + [viewController shouldPopViewControllerByBackButtonOrPopGesture:byPopGesture] == NO) { canPopViewController = NO; } return canPopViewController; } -- (void)resetSubviewsInNavBar:(UINavigationBar *)navBar { - if (@available(iOS 11, *)) { - } else { - // Workaround for >= iOS7.1. Thanks to @boliva - http://stackoverflow.com/posts/comments/34452906 - [navBar.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) { - if (subview.alpha < 1.0) { - [UIView animateWithDuration:.25 animations:^{ - subview.alpha = 1.0; - }]; +- (BOOL)shouldForceEnableInteractivePopGestureRecognizer { + UIViewController *viewController = [self topViewController]; + return self.viewControllers.count > 1 && self.interactivePopGestureRecognizer.enabled && [viewController respondsToSelector:@selector(forceEnableInteractivePopGestureRecognizer)] && [viewController forceEnableInteractivePopGestureRecognizer]; +} + +@end + + +@implementation _QMUINavigationInteractiveGestureDelegator + +- (instancetype)initWithParentViewController:(UINavigationController *)parentViewController { + if (self = [super init]) { + _parentViewController = parentViewController; + } + return self; +} + +#pragma mark - + +// iOS 13.4 开始会优先询问该方法,只有返回 YES 后才会继续后续的逻辑 +- (BOOL)_gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveEvent:(UIEvent *)event { + if (gestureRecognizer == self.parentViewController.interactivePopGestureRecognizer) { + NSObject *originGestureDelegate = self.parentViewController.qmui_interactivePopGestureRecognizerDelegate; + if ([originGestureDelegate respondsToSelector:_cmd]) { + BOOL originalValue = YES; + [originGestureDelegate qmui_performSelector:_cmd withPrimitiveReturnValue:&originalValue arguments:&gestureRecognizer, &event, nil]; + 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; + } } + return YES; } - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { - if (gestureRecognizer == self.interactivePopGestureRecognizer) { - self.tmp_topViewController = self.topViewController; - BOOL canPopViewController = [self canPopViewController:self.tmp_topViewController]; - if ([self shouldForceEnableInteractivePopGestureRecognizer]) { - // 如果是强制手势返回,则不会调用 navigationBar:shouldPopItem:(原因未知,不过好像也没什么影响),导致 pop 回去的上一层界面点击系统返回按钮时调用 [self canPopViewController:self.tmp_topViewController] 时里面的 self.tmp_topViewController 是上一个界面的值,所以提前把它设置为 nil - self.tmp_topViewController = nil; - } + if (gestureRecognizer == self.parentViewController.interactivePopGestureRecognizer) { + BOOL canPopViewController = [self.parentViewController canPopViewController:self.parentViewController.topViewController byPopGesture:YES]; if (canPopViewController) { - idoriginGestureDelegate = objc_getAssociatedObject(self, &originGestureDelegateKey); - if ([originGestureDelegate respondsToSelector:@selector(gestureRecognizerShouldBegin:)]) { - return [originGestureDelegate gestureRecognizerShouldBegin:gestureRecognizer]; + if ([self.parentViewController.qmui_interactivePopGestureRecognizerDelegate respondsToSelector:_cmd]) { + BOOL result = [self.parentViewController.qmui_interactivePopGestureRecognizerDelegate gestureRecognizerShouldBegin:gestureRecognizer]; + return result; } else { return NO; } @@ -164,17 +612,12 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { return YES; } -- (BOOL)shouldForceEnableInteractivePopGestureRecognizer { - UIViewController *viewController = [self topViewController]; - return self.viewControllers.count > 1 && self.interactivePopGestureRecognizer.enabled && [viewController respondsToSelector:@selector(forceEnableInteractivePopGestureRecognizer)] && [viewController forceEnableInteractivePopGestureRecognizer]; -} - - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { - if (gestureRecognizer == self.interactivePopGestureRecognizer) { - idoriginGestureDelegate = objc_getAssociatedObject(self, &originGestureDelegateKey); - if ([originGestureDelegate respondsToSelector:@selector(gestureRecognizer:shouldReceiveTouch:)]) { + if (gestureRecognizer == self.parentViewController.interactivePopGestureRecognizer) { + idoriginGestureDelegate = self.parentViewController.qmui_interactivePopGestureRecognizerDelegate; + if ([originGestureDelegate respondsToSelector:_cmd]) { BOOL originalValue = [originGestureDelegate gestureRecognizer:gestureRecognizer shouldReceiveTouch:touch]; - if (!originalValue && [self shouldForceEnableInteractivePopGestureRecognizer]) { + if (!originalValue && [self.parentViewController shouldForceEnableInteractivePopGestureRecognizer]) { return YES; } return originalValue; @@ -184,10 +627,10 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceive } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { - if (gestureRecognizer == self.interactivePopGestureRecognizer) { - idoriginGestureDelegate = objc_getAssociatedObject(self, &originGestureDelegateKey); - if ([originGestureDelegate respondsToSelector:@selector(gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)]) { - return [originGestureDelegate gestureRecognizer:gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer]; + if (gestureRecognizer == self.parentViewController.interactivePopGestureRecognizer) { + if ([self.parentViewController.qmui_interactivePopGestureRecognizerDelegate respondsToSelector:_cmd]) { + BOOL result = [self.parentViewController.qmui_interactivePopGestureRecognizerDelegate gestureRecognizer:gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer]; + return result; } } return NO; @@ -195,7 +638,7 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecogni // 是否要gestureRecognizer检测失败了,才去检测otherGestureRecognizer - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { - if (gestureRecognizer == self.interactivePopGestureRecognizer) { + if (gestureRecognizer == self.parentViewController.interactivePopGestureRecognizer) { // 如果只是实现了上面几个手势的delegate,那么返回的手势和当前界面上的scrollview或者其他存在的手势会冲突,所以如果判断是返回手势,则优先响应返回手势再响应其他手势。 // 不知道为什么,系统竟然没有实现这个delegate,那么它是怎么处理返回手势和其他手势的优先级的 return YES; @@ -207,5 +650,4 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequi @implementation UIViewController (BackBarButtonSupport) - @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UINavigationItem+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UINavigationItem+QMUI.h new file mode 100644 index 00000000..315514be --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UINavigationItem+QMUI.h @@ -0,0 +1,29 @@ +/** + * 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. + */ + +// +// UINavigationItem+QMUI.h +// qmui +// +// Created by QMUI Team on 2020/10/28. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UINavigationItem (QMUI) + +@property(nonatomic, weak, readonly, nullable) UINavigationBar *qmui_navigationBar; +@property(nonatomic, weak, readonly, nullable) UINavigationController *qmui_navigationController; +@property(nonatomic, weak, readonly, nullable) UIViewController *qmui_viewController; +@property(nonatomic, weak, readonly, nullable) UINavigationItem *qmui_previousItem; +@property(nonatomic, weak, readonly, nullable) UINavigationItem *qmui_nextItem; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UINavigationItem+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UINavigationItem+QMUI.m new file mode 100644 index 00000000..155f8cb5 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UINavigationItem+QMUI.m @@ -0,0 +1,68 @@ +/** + * 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. + */ + +// +// UINavigationItem+QMUI.m +// qmui +// +// Created by QMUI Team on 2020/10/28. +// + +#import "UINavigationItem+QMUI.h" +#import "UIView+QMUI.h" + +@implementation UINavigationItem (QMUI) + +- (UINavigationBar *)qmui_navigationBar { + // UINavigationItem 内部有个方法可以获取 navigationBar + if ([self respondsToSelector:@selector(navigationBar)]) { + return [self performSelector:@selector(navigationBar)]; + } + return nil; +} + +- (UINavigationController *)qmui_navigationController { + UINavigationBar *navigationBar = self.qmui_navigationBar; + UINavigationController *navigationController = (UINavigationController *)navigationBar.superview.qmui_viewController; + if ([navigationController isKindOfClass:UINavigationController.class]) { + return navigationController; + } + return nil; +} + +- (UIViewController *)qmui_viewController { + UINavigationBar *navigationBar = self.qmui_navigationBar; + UINavigationController *navigationController = self.qmui_navigationController; + + if (!navigationBar || !navigationController) return nil; + + NSInteger index = [navigationBar.items indexOfObject:self]; + if (index != NSNotFound && index < navigationController.viewControllers.count) { + UIViewController *viewController = navigationController.viewControllers[index]; + return viewController; + } + return nil; +} + +- (UINavigationItem *)qmui_previousItem { + NSArray *items = self.qmui_navigationBar.items; + if (!items.count) return nil; + NSInteger index = [items indexOfObject:self]; + if (index != NSNotFound && index > 0) return items[index - 1]; + return nil; +} + +- (UINavigationItem *)qmui_nextItem { + NSArray *items = self.qmui_navigationBar.items; + if (!items.count) return nil; + NSInteger index = [items indexOfObject:self]; + if (index != NSNotFound && index < items.count - 1) return items[index + 1]; + return nil; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIScrollView+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIScrollView+QMUI.h index 520c300c..c4005762 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIScrollView+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIScrollView+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIScrollView+QMUI.h @@ -15,6 +15,13 @@ #import +typedef NS_ENUM(NSInteger, QMUIScrollPosition) { + QMUIScrollPositionNone, // 滚动到临近的区域(可能是 Top 也可能是 Bottom) + QMUIScrollPositionTop, // 滚动到可视区域最顶部 + QMUIScrollPositionMiddle, // 滚动到可视区域中间 + QMUIScrollPositionBottom, // 滚动到可视区域底部 +}; + @interface UIScrollView (QMUI) /// 判断UIScrollView是否已经处于顶部(当UIScrollView内容不够多不可滚动时,也认为是在顶部) @@ -24,7 +31,13 @@ @property(nonatomic, assign, readonly) BOOL qmui_alreadyAtBottom; /// UIScrollView 的真正 inset,在 iOS11 以后需要用到 adjustedContentInset 而在 iOS11 以前只需要用 contentInset -@property(nonatomic, assign, readonly) UIEdgeInsets qmui_contentInset; +@property(nonatomic, assign, readonly) UIEdgeInsets qmui_contentInset DEPRECATED_MSG_ATTRIBUTE("请使用系统的 adjustedContentInset,QMUI 4.4.0 开始已不再支持 iOS 10,没必要提供该兼容性质的属性了,后续会删除。"); + +/** + UIScrollView 默认的 contentInset,会自动将 contentInset 和 scrollIndicatorInsets 都设置为这个值并且调用一次 qmui_scrollToTopUponContentInsetTopChange 设置默认的 contentOffset,一般用于 UIScrollViewContentInsetAdjustmentNever 的列表。 + @warning 如果 scrollView 被添加到某个 viewController 上,则只有在 viewController viewDidAppear 之前(不包含 viewDidAppear)设置这个属性才会自动滚到顶部,如果在 viewDidAppear 之后才添加到 viewController 上,则只有第一次设置 qmui_initialContentInset 时才会滚动到顶部。这样做的目的是为了避免在 scrollView 已经显示出来并滚动到列表中间后,由于某些原因,contentInset 发生了中间值的变动(也即一开始是正确的值,中间变成错误的值,再变回正确的值),此时列表会突然跳到顶部的问题。 + */ +@property(nonatomic, assign) UIEdgeInsets qmui_initialContentInset; /** * 判断当前的scrollView内容是否足够滚动 @@ -48,7 +61,7 @@ - (void)qmui_scrollToTop; /** - 滚到列表顶部,但如果 contentInset.top 与上一次相同则不会执行滚动操作,通常用于 UIScrollViewContentInsetAdjustmentNever 的 scrollView 设置完业务的 contentInset 后将列表滚到顶部。特别地,对于 UITableView,建议使用 qmui_initialContentInset。 + 滚到列表顶部,但如果 contentInset.top 与上一次相同则不会执行滚动操作,通常用于 UIScrollViewContentInsetAdjustmentNever 的 scrollView 设置完业务的 contentInset 后将列表滚到顶部。 */ - (void)qmui_scrollToTopUponContentInsetTopChange; @@ -62,7 +75,10 @@ /// 等同于[self qmui_scrollToBottomAnimated:NO] - (void)qmui_scrollToBottom; -// 立即停止滚动,用于那种手指已经离开屏幕但列表还在滚动的情况。 +/// 将 scroll 坐标系内的指定 rect 滚动到指定位置。 +- (void)qmui_scrollToRect:(CGRect)rect atPosition:(QMUIScrollPosition)scrollPosition animated:(BOOL)animated; + +/// 立即停止滚动,用于那种手指已经离开屏幕但列表还在滚动的情况。 - (void)qmui_stopDeceleratingIfNeeded; /** diff --git a/QMUI/QMUIKit/UIKitExtensions/UIScrollView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIScrollView+QMUI.m index 21553390..9ea0a5a0 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIScrollView+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIScrollView+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIScrollView+QMUI.m @@ -17,27 +17,59 @@ #import "QMUICore.h" #import "NSNumber+QMUI.h" #import "UIView+QMUI.h" +#import "UIViewController+QMUI.h" @interface UIScrollView () @property(nonatomic, assign) CGFloat qmuiscroll_lastInsetTopWhenScrollToTop; +@property(nonatomic, assign) BOOL qmuiscroll_hasSetInitialContentInset; @end @implementation UIScrollView (QMUI) QMUISynthesizeCGFloatProperty(qmuiscroll_lastInsetTopWhenScrollToTop, setQmuiscroll_lastInsetTopWhenScrollToTop) +QMUISynthesizeBOOLProperty(qmuiscroll_hasSetInitialContentInset, setQmuiscroll_hasSetInitialContentInset) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ExtendImplementationOfNonVoidMethodWithoutArguments([UIScrollView class], @selector(description), NSString *, ^NSString *(UIScrollView *selfObject, NSString *originReturnValue) { - return ([NSString stringWithFormat:@"%@, contentInset = %@", originReturnValue, NSStringFromUIEdgeInsets(selfObject.contentInset)]); + + OverrideImplementation([UIScrollView class], @selector(description), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^NSString *(UIScrollView *selfObject) { + // call super + NSString *(*originSelectorIMP)(id, SEL); + originSelectorIMP = (NSString *(*)(id, SEL))originalIMPProvider(); + NSString *result = originSelectorIMP(selfObject, originCMD); + + if (NSThread.isMainThread) { + result = ([NSString stringWithFormat:@"%@, contentInset = %@", result, NSStringFromUIEdgeInsets(selfObject.contentInset)]).mutableCopy; + } + return result; + }; }); + + if (QMUICMIActivated && AdjustScrollIndicatorInsetsByContentInsetAdjustment) { + OverrideImplementation([UIScrollView class], @selector(setContentInsetAdjustmentBehavior:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIScrollView *selfObject, UIScrollViewContentInsetAdjustmentBehavior firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, UIScrollViewContentInsetAdjustmentBehavior); + originSelectorIMP = (void (*)(id, SEL, UIScrollViewContentInsetAdjustmentBehavior))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if (firstArgv == UIScrollViewContentInsetAdjustmentNever) { + selfObject.automaticallyAdjustsScrollIndicatorInsets = NO; + } else { + selfObject.automaticallyAdjustsScrollIndicatorInsets = YES; + } + }; + }); + } }); } - (BOOL)qmui_alreadyAtTop { - if (((NSInteger)self.contentOffset.y) == -((NSInteger)self.qmui_contentInset.top)) { + if (CGFloatEqualToFloat(self.contentOffset.y, -self.adjustedContentInset.top)) { return YES; } @@ -49,7 +81,7 @@ - (BOOL)qmui_alreadyAtBottom { return YES; } - if (((NSInteger)self.contentOffset.y) == ((NSInteger)self.contentSize.height + self.qmui_contentInset.bottom - CGRectGetHeight(self.bounds))) { + if (CGFloatEqualToFloat(self.contentOffset.y, self.contentSize.height + self.adjustedContentInset.bottom - CGRectGetHeight(self.bounds))) { return YES; } @@ -57,11 +89,22 @@ - (BOOL)qmui_alreadyAtBottom { } - (UIEdgeInsets)qmui_contentInset { - if (@available(iOS 11, *)) { - return self.adjustedContentInset; - } else { - return self.contentInset; + return self.adjustedContentInset; +} + +static char kAssociatedObjectKey_initialContentInset; +- (void)setQmui_initialContentInset:(UIEdgeInsets)qmui_initialContentInset { + objc_setAssociatedObject(self, &kAssociatedObjectKey_initialContentInset, [NSValue valueWithUIEdgeInsets:qmui_initialContentInset], OBJC_ASSOCIATION_RETAIN_NONATOMIC); + self.contentInset = qmui_initialContentInset; + self.scrollIndicatorInsets = qmui_initialContentInset; + if (!self.qmuiscroll_hasSetInitialContentInset || !self.qmui_viewController || self.qmui_viewController.qmui_visibleState < QMUIViewControllerDidAppear) { + [self qmui_scrollToTopUponContentInsetTopChange]; } + self.qmuiscroll_hasSetInitialContentInset = YES; +} + +- (UIEdgeInsets)qmui_initialContentInset { + return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_initialContentInset)) UIEdgeInsetsValue]; } - (BOOL)qmui_canScroll { @@ -69,14 +112,14 @@ - (BOOL)qmui_canScroll { if (CGSizeIsEmpty(self.bounds.size)) { return NO; } - BOOL canVerticalScroll = self.contentSize.height + UIEdgeInsetsGetVerticalValue(self.qmui_contentInset) > CGRectGetHeight(self.bounds); - BOOL canHorizontalScoll = self.contentSize.width + UIEdgeInsetsGetHorizontalValue(self.qmui_contentInset) > CGRectGetWidth(self.bounds); + BOOL canVerticalScroll = self.contentSize.height + UIEdgeInsetsGetVerticalValue(self.adjustedContentInset) > CGRectGetHeight(self.bounds); + BOOL canHorizontalScoll = self.contentSize.width + UIEdgeInsetsGetHorizontalValue(self.adjustedContentInset) > CGRectGetWidth(self.bounds); return canVerticalScroll || canHorizontalScoll; } - (void)qmui_scrollToTopForce:(BOOL)force animated:(BOOL)animated { if (force || (!force && [self qmui_canScroll])) { - [self setContentOffset:CGPointMake(-self.qmui_contentInset.left, -self.qmui_contentInset.top) animated:animated]; + [self setContentOffset:CGPointMake(-self.adjustedContentInset.left, -self.adjustedContentInset.top) animated:animated]; } } @@ -97,7 +140,7 @@ - (void)qmui_scrollToTopUponContentInsetTopChange { - (void)qmui_scrollToBottomAnimated:(BOOL)animated { if ([self qmui_canScroll]) { - [self setContentOffset:CGPointMake(self.contentOffset.x, self.contentSize.height + self.qmui_contentInset.bottom - CGRectGetHeight(self.bounds)) animated:animated]; + [self setContentOffset:CGPointMake(self.contentOffset.x, self.contentSize.height + self.adjustedContentInset.bottom - CGRectGetHeight(self.bounds)) animated:animated]; } } @@ -117,4 +160,24 @@ - (void)qmui_setContentInset:(UIEdgeInsets)contentInset animated:(BOOL)animated } completion:nil]; } +- (void)qmui_scrollToRect:(CGRect)rect atPosition:(QMUIScrollPosition)scrollPosition animated:(BOOL)animated { + if (!self.qmui_canScroll) return; + BOOL fullyVisible = CGRectContainsRect(self.bounds, CGRectInsetEdges(rect, UIEdgeInsetsMake(0.5, 0.5, 0.5, 0.5)));// 四周故意减小一点点,避免小数点精度误差导致误以为无法 contains + if (fullyVisible) return; + if (scrollPosition == QMUIScrollPositionNone) { + [self scrollRectToVisible:rect animated:animated]; + return; + } + CGFloat targetY = self.contentOffset.y; + if (scrollPosition == QMUIScrollPositionTop) { + targetY = CGRectGetMinY(rect); + } else if (scrollPosition == QMUIScrollPositionBottom) { + targetY = CGRectGetMaxY(rect) - CGRectGetHeight(self.bounds); + } else if (scrollPosition == QMUIScrollPositionMiddle) { + targetY = CGRectGetMinY(rect) - (CGRectGetHeight(self.bounds) - CGRectGetHeight(rect)) / 2; + } + CGFloat offsetY = MIN(self.contentSize.height + self.adjustedContentInset.bottom - CGRectGetHeight(self.bounds), MAX(-self.adjustedContentInset.top, targetY)); + self.contentOffset = CGPointMake(self.contentOffset.x, offsetY); +} + @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UISearchBar+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UISearchBar+QMUI.h index 43fd1765..53c8cf46 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UISearchBar+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UISearchBar+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UISearchBar+QMUI.h @@ -16,16 +16,26 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + /** 提供更丰富的接口来修改 UISearchBar 的样式,注意大部分接口都同时支持配置表和 UIAppearance,如果有使用配置表并且该项的值不为 nil,则以配置表的值为准。 */ @interface UISearchBar (QMUI) +/** + 获取与 searchBar 关联的 UISearchController + */ +@property(nonatomic, strong, readonly) UISearchController *qmui_searchController; + /** 当以 tableHeaderView 的方式使用 UISearchBar 时,建议将这个属性置为 YES,从而可以帮你处理 https://github.com/Tencent/QMUI_iOS/issues/233 里列出的问题(抖动、iPhone X 适配等),默认为 NO */ @property(nonatomic, assign) BOOL qmui_usedAsTableHeaderView; +/// 是否让搜索框的 search icon、placeholder 在非搜索状态下居中(iOS 11 及以上,系统默认是居左的,iOS 10 及以下版本,系统默认就是居中),默认为 NO,也即维持系统默认表现不变。 +@property(nonatomic, assign) BOOL qmui_centerPlaceholder UI_APPEARANCE_SELECTOR; + /// 输入框内 placeholder 的颜色 @property(nullable, nonatomic, strong) UIColor *qmui_placeholderColor UI_APPEARANCE_SELECTOR; @@ -35,23 +45,72 @@ /// 输入框的文字字体,会同时影响 placeholder 的字体 @property(nullable, nonatomic, strong) UIFont *qmui_font UI_APPEARANCE_SELECTOR; -/// 输入框相对于系统原有布局位置的上下左右的偏移,正值表示向内缩小,负值表示向外扩大 +/// 输入框相对于系统原有布局位置的上下左右的偏移,正值表示向内缩小,负值表示向外扩大。注意输入框默认情况下就自带 (10, 8, 10, 8) 的间距,qmui_textFieldMargins 是基于这个间距的基础上做调整,换句话说,当 qmui_textFieldMargins 为 UIEdgeInsetsZero 时不代表输入框会上下左右都撑满父容器。 @property(nonatomic, assign) UIEdgeInsets qmui_textFieldMargins UI_APPEARANCE_SELECTOR; -@property(nullable, nonatomic, weak, readonly) UITextField *qmui_textField; +/// 支持根据 active 的值的不同来设置不一样的输入框位置偏移,当使用这个 block 后 @c qmui_textFieldMargins 无效。 +@property(nonatomic, copy) UIEdgeInsets (^qmui_textFieldMarginsBlock)(__kindof UISearchBar *searchBar, BOOL active); + +/// 当 UITableView 右侧出现 A-Z 那种索引条时,必要的情况下(例如全面屏 iPhone 的横屏状态,右侧已经存在较大的 safeAreaInsets,足以容纳 indexBar,则这种情况下系统就不会再调整了)系统会自动调整列表内容的布局(包括 sectionHeaderFooter、cell、作为 tableHeaderView 使用的 UISearchBar),在右侧腾出空间,以避免列表内容与 indexBar 重叠。 +/// 这个属性用于控制这种行为在 UISearchBar 里是否生效,默认为 YES,置为 NO 则可确保 UISearchBar 的布局在 indexBar 显示、隐藏时均保持一致,不产生跳动。弊端是如果屏幕较矮,且 indexBar 内容较多,则 searchBar 输入框右侧可能与 indexBar 产生重叠,请知悉。 +@property(nonatomic, assign) BOOL qmui_adjustTextFieldLayoutForIndexBar; /// 获取 searchBar 的背景 view,为一个 UIImageView 的子类 UISearchBarBackground,在 searchBar 初始化完即可被获取 @property(nullable, nonatomic, weak, readonly) UIView *qmui_backgroundView; -/// 获取 searchBar 内的取消按钮,注意 UISearchBar 的取消按钮是在 setShowsCancelButton:animated: 被调用之后才会生成 +/// 获取 searchBar 内的取消按钮,注意 UISearchBar 的取消按钮是在需要的时候才会生成(具体时机可以看 .m 内的 +load 方法) @property(nullable, nonatomic, weak, readonly) UIButton *qmui_cancelButton; /// 取消按钮的字体,由于系统的 cancelButton 是懒加载的,所以当不存在 cancelButton 时该值为 nil @property(nullable, nonatomic, strong) UIFont *qmui_cancelButtonFont UI_APPEARANCE_SELECTOR; +/// 取消按钮相对于系统原有布局位置的上下左右的偏移。 +@property(nonatomic, copy) UIEdgeInsets (^qmui_cancelButtonMarginsBlock)(__kindof UISearchBar *searchBar, BOOL active); + +/// 当 UISearchBar 被直接初始化后使用时(也即不存在关联的 UISearchController),cancelButton 只有在 searchBar 聚焦升起键盘时才是 enabled,键盘降下时就 disabled。通常这不是我们想要的,所以提供这个开关,允许你强制保持 cancelButton 一直为 enabled。 +/// 默认为 YES。 +/// @note 注意只有 searchBar 不存在关联的 UISearchController 时,这个属性才会生效。 +@property(nonatomic, assign) BOOL qmui_alwaysEnableCancelButton UI_APPEARANCE_SELECTOR; + /// 获取 scopeBar 里的 UISegmentedControl @property(nullable, nonatomic, weak, readonly) UISegmentedControl *qmui_segmentedControl; +/// 控制 @c qmui_leftAccessoryView 的显隐,默认为 YES,仅当 @c qmui_leftAccessoryView 有值时才生效 +@property(nonatomic, assign) BOOL qmui_showsLeftAccessoryView; +- (void)qmui_setShowsLeftAccessoryView:(BOOL)showsLeftAccessoryView animated:(BOOL)animated; + +/// 在 searchBar 的输入框左边显示一个 view,当显示该 view 时会调用该 view 的 sizeToFit 来确定 view 的大小。注意系统默认行为是 UISearchBar 内只有 UIButton 类型的 view 才能接受点击事件,其他类型的 view 点击都是进入搜索状态。 +@property(nonatomic, strong) UIView *qmui_leftAccessoryView; + +/// 调整 @c qmui_leftAccessoryView 的布局,默认为 UIEdgeInsetsZero,也即左边贴紧 searchBar 边缘,右边与 textField 之间间隔系统默认的 8,垂直方向与 textField 居中。 +@property(nonatomic, assign) UIEdgeInsets qmui_leftAccessoryViewMargins UI_APPEARANCE_SELECTOR; + +/// 控制 @c qmui_rightAccessoryView 的显隐,默认为 YES,仅当 @c qmui_rightAccessoryView 有值时才生效 +@property(nonatomic, assign) BOOL qmui_showsRightAccessoryView; +- (void)qmui_setShowsRightAccessoryView:(BOOL)showsRightAccessoryView animated:(BOOL)animated; + +/// 在 searchBar 的输入框右边显示一个 view,当显示该 view 时会调用该 view 的 sizeToFit 来确定 view 的大小。注意系统默认行为是 UISearchBar 内只有 UIButton 类型的 view 才能接受点击事件,其他类型的 view 点击都是进入搜索状态。 +@property(nonatomic, strong) UIView *qmui_rightAccessoryView; + +/// 调整 @c qmui_rightAccessoryView 的布局,默认为 UIEdgeInsetsZero,也即左边与 textField 之间间隔系统默认的 8,右边贴紧 searchBar 边缘,垂直方向与 textField 居中。 +@property(nonatomic, assign) UIEdgeInsets qmui_rightAccessoryViewMargins UI_APPEARANCE_SELECTOR; + +/// 修复当 UISearchController.searchBar 被当做 tableHeaderView 使用时可能产生的布局问题 +/// https://github.com/Tencent/QMUI_iOS/issues/950 +@property(nonatomic, assign) BOOL qmui_fixMaskViewLayoutBugAutomatically; + +/// 是否需要自动修复 UISearchController.searchBar 作为 UITableView.tableHeaderView 时进入搜索状态,搜索结果列表顶部有一大片空白的 bug,默认为 YES。 +/// https://github.com/Tencent/QMUI_iOS/issues/1473 +@property(nonatomic, assign) BOOL qmui_shouldFixSearchResultsContentInset; + - (void)qmui_styledAsQMUISearchBar; +/// 生成指定颜色的搜索框输入框背景图,大小与系统默认的保持一致,只是颜色不同 ++ (nullable UIImage *)qmui_generateTextFieldBackgroundImageWithColor:(nullable UIColor *)color; + +/// 生成指定背景色和底部边框颜色的搜索框背景图 ++ (nullable UIImage *)qmui_generateBackgroundImageWithColor:(nullable UIColor *)backgroundColor borderColor:(nullable UIColor *)borderColor; + @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m index 8d8e4d43..4e50a0b5 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UISearchBar+QMUI.m @@ -17,118 +17,283 @@ #import "QMUICore.h" #import "UIImage+QMUI.h" #import "UIView+QMUI.h" +#import "UIViewController+QMUI.h" -#define SearchBarActiveHeightIOS11Later (IS_NOTCHED_SCREEN ? 55.0f : 50.0f) -#define SearchBarNormalHeightIOS11Later 56.0f +@interface UISearchBar () +@property(nonatomic, assign) CGFloat qmuisb_centerPlaceholderCachedWidth1; +@property(nonatomic, assign) CGFloat qmuisb_centerPlaceholderCachedWidth2; +@property(nonatomic, assign) UIEdgeInsets qmuisb_customTextFieldMargins; +@end @implementation UISearchBar (QMUI) QMUISynthesizeBOOLProperty(qmui_usedAsTableHeaderView, setQmui_usedAsTableHeaderView) -QMUISynthesizeUIEdgeInsetsProperty(qmui_textFieldMargins, setQmui_textFieldMargins) +QMUISynthesizeBOOLProperty(qmui_alwaysEnableCancelButton, setQmui_alwaysEnableCancelButton) +QMUISynthesizeBOOLProperty(qmui_fixMaskViewLayoutBugAutomatically, setQmui_fixMaskViewLayoutBugAutomatically) +QMUISynthesizeBOOLProperty(qmui_shouldFixSearchResultsContentInset, setQmui_shouldFixSearchResultsContentInset) +QMUISynthesizeUIEdgeInsetsProperty(qmuisb_customTextFieldMargins, setQmuisb_customTextFieldMargins) +QMUISynthesizeCGFloatProperty(qmuisb_centerPlaceholderCachedWidth1, setQmuisb_centerPlaceholderCachedWidth1) +QMUISynthesizeCGFloatProperty(qmuisb_centerPlaceholderCachedWidth2, setQmuisb_centerPlaceholderCachedWidth2) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ExtendImplementationOfVoidMethodWithTwoArguments([UISearchBar class], @selector(setShowsCancelButton:animated:), BOOL, BOOL, ^(UISearchBar *selfObject, BOOL firstArgv, BOOL secondArgv) { - if (selfObject.qmui_cancelButton && selfObject.qmui_cancelButtonFont) { - selfObject.qmui_cancelButton.titleLabel.font = selfObject.qmui_cancelButtonFont; + void (^setupCancelButtonBlock)(UISearchBar *, UIButton *) = ^void(UISearchBar *searchBar, UIButton *cancelButton) { + if (searchBar.qmui_alwaysEnableCancelButton && !searchBar.qmui_searchController) { + cancelButton.enabled = YES; + } + + if (cancelButton && searchBar.qmui_cancelButtonFont) { + cancelButton.titleLabel.font = searchBar.qmui_cancelButtonFont; } + + if (cancelButton && !cancelButton.qmui_frameWillChangeBlock) { + __weak __typeof(searchBar)weakSearchBar = searchBar; + cancelButton.qmui_frameWillChangeBlock = ^CGRect(UIButton *aCancelButton, CGRect followingFrame) { + return [weakSearchBar qmuisb_adjustCancelButtonFrame:followingFrame]; + }; + } + }; + + // iOS 13 开始 UISearchBar 内部的输入框、取消按钮等 subviews 都由这个 class 创建、管理 + ExtendImplementationOfVoidMethodWithoutArguments(NSClassFromString(@"_UISearchBarVisualProviderIOS"), NSSelectorFromString(@"setUpCancelButton"), ^(NSObject *selfObject) { + UIButton *cancelButton = [selfObject qmui_valueForKey:@"cancelButton"]; + UISearchBar *searchBar = (UISearchBar *)cancelButton.superview.superview.superview; + QMUIAssert([searchBar isKindOfClass:UISearchBar.class], @"UISearchBar (QMUI)", @"Can not find UISearchBar from cancelButton"); + setupCancelButtonBlock(searchBar, cancelButton); + }); + + OverrideImplementation(NSClassFromString(@"UINavigationButton"), @selector(setEnabled:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIButton *selfObject, BOOL firstArgv) { + + UISearchBar *searchBar = (UISearchBar *)selfObject.superview.superview.superview;; + if ([searchBar isKindOfClass:UISearchBar.class] && searchBar.qmui_alwaysEnableCancelButton && !searchBar.qmui_searchController) { + firstArgv = YES; + } + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + }; }); ExtendImplementationOfVoidMethodWithSingleArgument([UISearchBar class], @selector(setPlaceholder:), NSString *, (^(UISearchBar *selfObject, NSString *placeholder) { if (selfObject.qmui_placeholderColor || selfObject.qmui_font) { - NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init]; + NSMutableAttributedString *string = selfObject.searchTextField.attributedPlaceholder.mutableCopy; if (selfObject.qmui_placeholderColor) { - attributes[NSForegroundColorAttributeName] = selfObject.qmui_placeholderColor; + [string addAttribute:NSForegroundColorAttributeName value:selfObject.qmui_placeholderColor range:NSMakeRange(0, string.length)]; } if (selfObject.qmui_font) { - attributes[NSFontAttributeName] = selfObject.qmui_font; + [string addAttribute:NSFontAttributeName value:selfObject.qmui_font range:NSMakeRange(0, string.length)]; } - selfObject.qmui_textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:placeholder attributes:attributes]; + // 默认移除文字阴影 + [string removeAttribute:NSShadowAttributeName range:NSMakeRange(0, string.length)]; + selfObject.searchTextField.attributedPlaceholder = string.copy; } })); - ExtendImplementationOfVoidMethodWithoutArguments([UISearchBar class], @selector(layoutSubviews), ^(UISearchBar *selfObject) { - [selfObject fixLandscapeStyle]; + // iOS 13 下,UISearchBar 内的 UITextField 的 _placeholderLabel 会在 didMoveToWindow 时被重新设置 textColor,导致我们在 searchBar 添加到界面之前设置的 placeholderColor 失效,所以在这里重新设置一遍 + // https://github.com/Tencent/QMUI_iOS/issues/830 + ExtendImplementationOfVoidMethodWithoutArguments([UISearchBar class], @selector(didMoveToWindow), ^(UISearchBar *selfObject) { + if (selfObject.qmui_placeholderColor) { + selfObject.placeholder = selfObject.placeholder; + } + }); + + // -[_UISearchBarLayout applyLayout] 是 iOS 13 系统新增的方法,该方法可能会在 -[UISearchBar layoutSubviews] 后调用,作进一步的布局调整。 + Class _UISearchBarLayoutClass = NSClassFromString([NSString stringWithFormat:@"_%@%@",@"UISearchBar", @"Layout"]); + OverrideImplementation(_UISearchBarLayoutClass, NSSelectorFromString(@"applyLayout"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject) { + + // call super + void (^callSuperBlock)(void) = ^{ + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + }; + + UISearchBar *searchBar = (UISearchBar *)((UIView *)[selfObject qmui_valueForKey:[NSString stringWithFormat:@"_%@",@"searchBarBackground"]]).superview.superview; + + QMUIAssert(searchBar == nil || [searchBar isKindOfClass:[UISearchBar class]], @"UISearchBar (QMUI)", @"not a searchBar"); + + if (searchBar && searchBar.qmui_searchController.isBeingDismissed && searchBar.qmui_usedAsTableHeaderView) { + CGRect previousRect = searchBar.qmui_backgroundView.frame; + callSuperBlock(); + // applyLayout 方法中会修改 _searchBarBackground 的 frame ,从而覆盖掉 qmui_usedAsTableHeaderView 做出的调整,所以这里还原本次修改。 + searchBar.qmui_backgroundView.frame = previousRect; + } else { + callSuperBlock(); + } + }; - [selfObject fixDismissingAnimation]; + }); + + if (@available(iOS 14.0, *)) { + // iOS 14 beta 1 修改了 searchTextField 的 font 属性会导致 TextField 高度异常,从而导致 searchBarContainerView 的高度异常,临时修复一下 + Class _UISearchBarContainerViewClass = NSClassFromString([NSString stringWithFormat:@"_%@%@",@"UISearchBar", @"ContainerView"]); + OverrideImplementation(_UISearchBarContainerViewClass, @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, CGRect frame) { + UISearchBar *searchBar = selfObject.subviews.firstObject; + if ([searchBar isKindOfClass:[UISearchBar class]]) { + if (searchBar.qmuisb_shouldFixLayoutWhenUsedAsTableHeaderView && searchBar.qmui_isActive) { + // 刘海屏即使隐藏了 statusBar 也不会影响 containerView 的高度,要把 statusBar 计算在内 + CGFloat currentStatusBarHeight = IS_NOTCHED_SCREEN ? StatusBarHeightConstant : StatusBarHeight; + if (frame.origin.y < currentStatusBarHeight + NavigationBarHeight) { + // 非刘海屏在隐藏了 statusBar 后,如果只计算激活时的高度则为 50,这种情况下应该取 56 + frame.size.height = MAX(UISearchBar.qmuisb_seachBarDefaultActiveHeight + currentStatusBarHeight, 56); + frame.origin.y = 0; + } + } + } + void (*originSelectorIMP)(id, SEL, CGRect); + originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, frame); + }; + }); + } + + // -[UISearchBarTextField setFrame:] + OverrideImplementation(NSClassFromString([NSString stringWithFormat:@"%@%@",@"UISearchBarText", @"Field"]), @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITextField *textField, CGRect frame) { + UISearchBar *searchBar = (UISearchBar *)textField.superview.superview.superview;; + QMUIAssert(searchBar == nil || [searchBar isKindOfClass:[UISearchBar class]], @"UISearchBar (QMUI)", @"not a searchBar"); + if (searchBar) { + frame = [searchBar qmuisb_adjustedSearchTextFieldFrameByOriginalFrame:frame]; + } + + void (*originSelectorIMP)(id, SEL, CGRect); + originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); + originSelectorIMP(textField, originCMD, frame); + + [searchBar qmuisb_searchTextFieldFrameDidChange]; + }; + }); + + ExtendImplementationOfVoidMethodWithoutArguments([UISearchBar class], @selector(layoutSubviews), ^(UISearchBar *selfObject) { - if (!UIEdgeInsetsEqualToEdgeInsets(selfObject.qmui_textFieldMargins, UIEdgeInsetsZero)) { - selfObject.qmui_textField.frame = CGRectInsetEdges(selfObject.qmui_textField.frame, selfObject.qmui_textFieldMargins); + // 修复 iOS 13 backgroundView 没有撑开到顶部的问题 + if (IOS_VERSION >= 13.0 && selfObject.qmui_usedAsTableHeaderView && selfObject.qmui_isActive) { + selfObject.qmui_backgroundView.qmui_height = StatusBarHeightConstant + selfObject.qmui_height; + selfObject.qmui_backgroundView.qmui_top = -StatusBarHeightConstant; } + [selfObject qmuisb_fixDismissingAnimationIfNeeded]; + [selfObject qmuisb_fixSearchResultsScrollViewContentInsetIfNeeded]; - CGFloat textFieldCornerRadius = SearchBarTextFieldCornerRadius; - if (textFieldCornerRadius != 0) { - textFieldCornerRadius = textFieldCornerRadius > 0 ? textFieldCornerRadius : CGRectGetHeight(selfObject.qmui_textField.frame) / 2.0; - } - selfObject.qmui_textField.layer.cornerRadius = textFieldCornerRadius; - selfObject.qmui_textField.clipsToBounds = textFieldCornerRadius != 0; }); OverrideImplementation([UISearchBar class], @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UISearchBar *selfObject, CGRect frame) { + frame = [selfObject qmuisb_adjustedSearchBarFrameByOriginalFrame:frame]; + // call super - void (^callSuperBlock)(CGRect) = ^void(CGRect aFrame) { - void (*originSelectorIMP)(id, SEL, CGRect); - originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, aFrame); - }; + void (*originSelectorIMP)(id, SEL, CGRect); + originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, frame); - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (!selfObject.qmui_usedAsTableHeaderView) { - callSuperBlock(frame); - return; - } - - // 重写 setFrame: 是为了这个 issue:https://github.com/Tencent/QMUI_iOS/issues/233 - - if (@available(iOS 11, *)) { - // iOS 11 下用 tableHeaderView 的方式使用 searchBar 的话,进入搜索状态时 y 偏上了,导致间距错乱 - - if (![selfObject qmui_isActive]) { - callSuperBlock(frame); - return; - } - - if (IS_NOTCHED_SCREEN) { - // 竖屏 - if (CGRectGetMinY(frame) == 38) { - // searching - frame = CGRectSetY(frame, 44); - } - - // 横屏 - if (CGRectGetMinY(frame) == -6) { - frame = CGRectSetY(frame, 0); - } - } else { - - // 竖屏 - if (CGRectGetMinY(frame) == 14) { - frame = CGRectSetY(frame, 20); - } - - // 横屏 - if (CGRectGetMinY(frame) == -6) { - frame = CGRectSetY(frame, 0); - } - } - // 强制在激活状态下 高度也为 56,方便后续做平滑过渡动画 (iOS 11 默认下,非刘海屏的机器激活后为 50,刘海屏激活后为 55) - if (frame.size.height == SearchBarActiveHeightIOS11Later) { - frame.size.height = 56; - } - } + }; + }); + + // [UIKit Bug] 当 UISearchController.searchBar 作为 tableHeaderView 使用时,顶部可能出现 1px 的间隙导致露出背景色 + // https://github.com/Tencent/QMUI_iOS/issues/950 + OverrideImplementation([UISearchBar class], NSSelectorFromString(@"_setMaskBounds:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISearchBar *selfObject, CGRect firstArgv) { + + BOOL shouldFixBug = selfObject.qmui_fixMaskViewLayoutBugAutomatically + && selfObject.qmui_searchController + && [selfObject.superview isKindOfClass:UITableView.class] + && ((UITableView *)selfObject.superview).tableHeaderView == selfObject; + if (shouldFixBug) { + firstArgv = CGRectMake(CGRectGetMinX(firstArgv), CGRectGetMinY(firstArgv) - PixelOne, CGRectGetWidth(firstArgv), CGRectGetHeight(firstArgv) + PixelOne); } - callSuperBlock(frame); + + // call super + void (*originSelectorIMP)(id, SEL, CGRect); + originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); }; }); + + // [UIKit Bug] 将 UISearchBar 作为 UITableView.tableHeaderView 使用时,如果列表内容不满一屏,可能出现搜索框不可视的问题 + // https://github.com/Tencent/QMUI_iOS/issues/1207 + ExtendImplementationOfVoidMethodWithoutArguments([UISearchBar class], @selector(didMoveToSuperview), ^(UISearchBar *selfObject) { + if (selfObject.superview && CGRectGetHeight(selfObject.subviews.firstObject.frame) != CGRectGetHeight(selfObject.bounds)) { + BeginIgnorePerformSelectorLeaksWarning + [selfObject.qmui_searchController performSelector:NSSelectorFromString([NSString stringWithFormat:@"%@%@MaskIfNecessary", @"_update", @"SearchBar"])]; + EndIgnorePerformSelectorLeaksWarning + } + }); + + ExtendImplementationOfNonVoidMethodWithSingleArgument([UISearchBar class], @selector(initWithFrame:), CGRect, UISearchBar *, ^UISearchBar *(UISearchBar *selfObject, CGRect firstArgv, UISearchBar *originReturnValue) { + [originReturnValue qmuisb_didInitialize]; + return originReturnValue; + }); + + ExtendImplementationOfNonVoidMethodWithSingleArgument([UISearchBar class], @selector(initWithCoder:), NSCoder *, UISearchBar *, ^UISearchBar *(UISearchBar *selfObject, NSCoder *firstArgv, UISearchBar *originReturnValue) { + [originReturnValue qmuisb_didInitialize]; + return originReturnValue; + }); }); } +- (void)qmuisb_didInitialize { + self.qmui_alwaysEnableCancelButton = YES; + self.qmui_showsLeftAccessoryView = YES; + self.qmui_showsRightAccessoryView = YES; + self.qmui_shouldFixSearchResultsContentInset = YES; + if (QMUICMIActivated && ShouldFixSearchBarMaskViewLayoutBug) { + self.qmui_fixMaskViewLayoutBugAutomatically = YES; + } +} + +static char kAssociatedObjectKey_centerPlaceholder; +- (void)setQmui_centerPlaceholder:(BOOL)qmui_centerPlaceholder { + objc_setAssociatedObject(self, &kAssociatedObjectKey_centerPlaceholder, @(qmui_centerPlaceholder), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + __weak __typeof(self)weakSelf = self; + if (qmui_centerPlaceholder) { + self.searchTextField.qmui_layoutSubviewsBlock = ^(UITextField * _Nonnull textField) { + + // 某些中间状态 textField 的宽度会出现负值,但由于 CGRectGetWidth() 一定是返回正值的,所以这里必须用 bounds.size.width 的方式取值,而不是用 CGRectGetWidth() + if (textField.bounds.size.width <= 0) return; + + if (textField.isEditing || textField.text.length > 0) { + weakSelf.qmuisb_centerPlaceholderCachedWidth1 = 0; + weakSelf.qmuisb_centerPlaceholderCachedWidth2 = 0; + if (!UIOffsetEqualToOffset(UIOffsetZero, [weakSelf positionAdjustmentForSearchBarIcon:UISearchBarIconSearch])) { + [weakSelf setPositionAdjustment:UIOffsetZero forSearchBarIcon:UISearchBarIconSearch]; + [textField layoutIfNeeded];// 在切换搜索状态时要让 positionAdjustment 立即生效,才能看到动画效果 + } + } else { + UIView *leftView = [textField qmui_valueForKey:@"leftView"]; + UILabel *label = [textField qmui_valueForKey:@"placeholderLabel"]; + CGFloat width = CGRectGetMaxX(label.frame) - CGRectGetMinX(leftView.frame); + if (fabs(CGRectGetWidth(textField.bounds) - weakSelf.qmuisb_centerPlaceholderCachedWidth1) > 1 || fabs(width - weakSelf.qmuisb_centerPlaceholderCachedWidth2) > 1) { + weakSelf.qmuisb_centerPlaceholderCachedWidth1 = CGRectGetWidth(textField.bounds); + weakSelf.qmuisb_centerPlaceholderCachedWidth2 = width; + CGFloat searchIconDefaultMarginLeft = 6; // 系统的放大镜 icon 默认距离 textField 左边就是这个值,计算居中时要考虑进去,因为 positionAdjustment 是基于系统默认布局的基础上做偏移的 + CGFloat horizontal = (weakSelf.qmuisb_centerPlaceholderCachedWidth1 - weakSelf.qmuisb_centerPlaceholderCachedWidth2) / 2.0 - searchIconDefaultMarginLeft;// 这里没有用 CGFloatGetCenter 是为了避免 iOS 12 及以下 iPhone 8 Plus tableView 显示右边的索引条时,每次算出来都差1,第一次49第二次50第三次49...陷入死循环,干脆不要操作精度取整 + [weakSelf setPositionAdjustment:UIOffsetMake(horizontal, 0) forSearchBarIcon:UISearchBarIconSearch]; + [textField layoutIfNeeded];// 在切换搜索状态时要让 positionAdjustment 立即生效,才能看到动画效果 + } + } + }; + [self.searchTextField setNeedsLayout]; + } else { + self.searchTextField.qmui_layoutSubviewsBlock = nil; + self.qmuisb_centerPlaceholderCachedWidth1 = 0; + self.qmuisb_centerPlaceholderCachedWidth2 = 0; + [self setPositionAdjustment:UIOffsetZero forSearchBarIcon:UISearchBarIconSearch]; + } +} + +- (BOOL)qmui_centerPlaceholder { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_centerPlaceholder)) boolValue]; +} + static char kAssociatedObjectKey_PlaceholderColor; - (void)setQmui_placeholderColor:(UIColor *)qmui_placeholderColor { objc_setAssociatedObject(self, &kAssociatedObjectKey_PlaceholderColor, qmui_placeholderColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); @@ -145,7 +310,7 @@ - (UIColor *)qmui_placeholderColor { static char kAssociatedObjectKey_TextColor; - (void)setQmui_textColor:(UIColor *)qmui_textColor { objc_setAssociatedObject(self, &kAssociatedObjectKey_TextColor, qmui_textColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - self.qmui_textField.textColor = qmui_textColor; + self.searchTextField.textColor = qmui_textColor; } - (UIColor *)qmui_textColor { @@ -161,20 +326,15 @@ - (void)setQmui_font:(UIFont *)qmui_font { } // 更新输入框的文字样式 - self.qmui_textField.font = qmui_font; + self.searchTextField.font = qmui_font; } - (UIFont *)qmui_font { return (UIFont *)objc_getAssociatedObject(self, &kAssociatedObjectKey_font); } -- (UITextField *)qmui_textField { - UITextField *textField = [self valueForKey:@"searchField"]; - return textField; -} - - (UIButton *)qmui_cancelButton { - UIButton *cancelButton = [self valueForKey:@"cancelButton"]; + UIButton *cancelButton = [self qmui_valueForKey:@"cancelButton"]; return cancelButton; } @@ -188,9 +348,67 @@ - (UIFont *)qmui_cancelButtonFont { return (UIFont *)objc_getAssociatedObject(self, &kAssociatedObjectKey_cancelButtonFont); } +static char kAssociatedObjectKey_cancelButtonMarginsBlock; +- (void)setQmui_cancelButtonMarginsBlock:(UIEdgeInsets (^)(__kindof UISearchBar * _Nonnull, BOOL))qmui_cancelButtonMarginsBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_cancelButtonMarginsBlock, qmui_cancelButtonMarginsBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + [self.qmui_cancelButton.superview setNeedsLayout]; +} + +- (UIEdgeInsets (^)(__kindof UISearchBar * _Nonnull, BOOL))qmui_cancelButtonMarginsBlock { + return (UIEdgeInsets (^)(__kindof UISearchBar * _Nonnull, BOOL))objc_getAssociatedObject(self, &kAssociatedObjectKey_cancelButtonMarginsBlock); +} + +static char kAssociatedObjectKey_textFieldMargins; +- (void)setQmui_textFieldMargins:(UIEdgeInsets)qmui_textFieldMargins { + objc_setAssociatedObject(self, &kAssociatedObjectKey_textFieldMargins, @(qmui_textFieldMargins), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self qmuisb_setNeedsLayoutTextField]; +} + +- (UIEdgeInsets)qmui_textFieldMargins { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_textFieldMargins)) UIEdgeInsetsValue]; +} + +static char kAssociatedObjectKey_textFieldMarginsBlock; +- (void)setQmui_textFieldMarginsBlock:(UIEdgeInsets (^)(__kindof UISearchBar * _Nonnull, BOOL))qmui_textFieldMarginsBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_textFieldMarginsBlock, qmui_textFieldMarginsBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + [self qmuisb_setNeedsLayoutTextField]; +} + +- (UIEdgeInsets (^)(__kindof UISearchBar * _Nonnull, BOOL))qmui_textFieldMarginsBlock { + return (UIEdgeInsets (^)(__kindof UISearchBar * _Nonnull, BOOL))objc_getAssociatedObject(self, &kAssociatedObjectKey_textFieldMarginsBlock); +} + +static char kAssociatedObjectKey_adjustTextFieldLayoutForIndexBar; +- (void)setQmui_adjustTextFieldLayoutForIndexBar:(BOOL)adjustTextFieldLayoutForIndexBar { + objc_setAssociatedObject(self, &kAssociatedObjectKey_adjustTextFieldLayoutForIndexBar, @(adjustTextFieldLayoutForIndexBar), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (!adjustTextFieldLayoutForIndexBar) { + [QMUIHelper executeBlock:^{ + // 系统内部的调用关系是:-[UITableView reloadData]→-[UITableView _updateIndexFrame]→[tableHeaderView isKindOfClass:UISearchBar]→-[UISearchBar _updateInsetsForTableView:]→-[UITableView _indexBarExtentFromEdge],所以只需要跳过 _updateInsetsForTableView: 即可屏蔽该特性 + // - [UISearchBar _updateInsetsForTableView:] + // - (void) _updateInsetsForTableView:(id)arg1; (0x184a14f24) + OverrideImplementation([UISearchBar class], NSSelectorFromString([NSString qmui_stringByConcat:@"_", @"updateInsets", @"ForTableView", @":", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISearchBar *selfObject, UITableView *firstArgv) { + + if (!selfObject.qmui_adjustTextFieldLayoutForIndexBar) return; + + // call super + void (*originSelectorIMP)(id, SEL, UITableView *); + originSelectorIMP = (void (*)(id, SEL, UITableView *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + }; + }); + } oncePerIdentifier:@"UISearchBar (QMUI) adjustIndexBar"]; + } +} + +- (BOOL)qmui_adjustTextFieldLayoutForIndexBar { + NSNumber *value = (NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_adjustTextFieldLayoutForIndexBar); + if (!value) return YES; + return value.boolValue; +} + - (UISegmentedControl *)qmui_segmentedControl { - // 注意,segmentedControl 只是整条 scopeBar 里的一部分,虽然它的 key 叫做“scopeBar” - UISegmentedControl *segmentedControl = [self valueForKey:@"scopeBar"]; + UISegmentedControl *segmentedControl = [self qmui_valueForKey:@"scopeBar"]; return segmentedControl; } @@ -199,140 +417,29 @@ - (BOOL)qmui_isActive { } - (UISearchController *)qmui_searchController { - return [self valueForKey:@"_searchController"]; -} - -- (void)fixLandscapeStyle { - if (self.qmui_usedAsTableHeaderView) { - if (@available(iOS 11, *)) { - if ([self qmui_isActive] && IS_LANDSCAPE) { - // 11.0 及以上的版本,横屏时,searchBar 内部的内容布局会偏上,所以这里强制居中一下 - CGFloat fixedOffset = (SearchBarActiveHeightIOS11Later - SearchBarNormalHeightIOS11Later) / 2.0; - self.qmui_textField.frame = CGRectSetY(self.qmui_textField.frame, self.qmui_textField.qmui_topWhenCenterInSuperview + fixedOffset); - self.qmui_cancelButton.frame = CGRectSetY(self.qmui_cancelButton.frame, self.qmui_cancelButton.qmui_topWhenCenterInSuperview + fixedOffset); - if (self.qmui_segmentedControl.superview.qmui_top < self.qmui_textField.qmui_bottom) { - // scopeBar 显示在搜索框右边 - self.qmui_segmentedControl.superview.qmui_top = self.qmui_segmentedControl.superview.qmui_topWhenCenterInSuperview + fixedOffset; - } - } - } - } -} - -- (void)fixDismissingAnimation { - if (self.qmui_usedAsTableHeaderView) { - if (@available(iOS 11, *)) { - if (self.qmui_searchController.isBeingDismissed) { - self.qmui_textField.superview.qmui_height = SearchBarNormalHeightIOS11Later; - self.qmui_textField.frame = CGRectSetY(self.qmui_textField.frame, self.qmui_textField.qmui_topWhenCenterInSuperview); - self.qmui_backgroundView.frame = self.qmui_textField.superview.bounds; - if (IS_NOTCHED_SCREEN && self.frame.origin.y == 43) { // 修复刘海屏下,系统计算少了一个 px - self.frame = CGRectSetY(self.frame, StatusBarHeightConstant); - } - - UIView *searchBarContainerView = self.superview; - if (searchBarContainerView.layer.masksToBounds) { - searchBarContainerView.layer.masksToBounds = NO; - if (self.showsScopeBar && !IS_LANDSCAPE) { - //竖屏并且显示了 ScopeBar 系统可以自然过渡,无需添加 mask 动画 - return; - } - // 之所以给 searchBarContainerView 设置mask 动画, 而非 backgroundView 是因为 searchBarContainerView 在每次激活都会重新创建一个,修改 masksToBounds 和设置动画不会去影响其他东西 - CAShapeLayer *maskLayer = [CAShapeLayer layer]; - CGMutablePathRef path = CGPathCreateMutable(); - CGPathAddRect(path, NULL, CGRectMake(0, 0, searchBarContainerView.qmui_width, StatusBarHeight + SearchBarActiveHeightIOS11Later)); - maskLayer.path = path; - searchBarContainerView.layer.mask = maskLayer; - - CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"]; - CGMutablePathRef animationPath = CGPathCreateMutable(); - CGPathAddRect(animationPath, NULL, CGRectMake(0, 0, searchBarContainerView.qmui_width, StatusBarHeight + SearchBarNormalHeightIOS11Later)); - animation.toValue = (__bridge id)animationPath; - animation.removedOnCompletion = NO; - animation.fillMode = kCAFillModeForwards; - [searchBarContainerView.layer.mask addAnimation:animation forKey:nil]; - } - } - } - } + return [self qmui_valueForKey:@"_searchController"]; } - (UIView *)qmui_backgroundView { - UIView *backgroundView = [self valueForKey:@"background"]; + BeginIgnorePerformSelectorLeaksWarning + UIView *backgroundView = [self performSelector:NSSelectorFromString(@"_backgroundView")]; + EndIgnorePerformSelectorLeaksWarning return backgroundView; } -- (void)qmuisb_setFrame:(CGRect)frame { - - if (!self.qmui_usedAsTableHeaderView) { - [self qmuisb_setFrame:frame]; - return; - } - - // 重写 setFrame: 是为了这个 issue:https://github.com/Tencent/QMUI_iOS/issues/233 - - if (@available(iOS 11, *)) { - // iOS 11 下用 tableHeaderView 的方式使用 searchBar 的话,进入搜索状态时 y 偏上了,导致间距错乱 - - if (![self qmui_isActive]) { - [self qmuisb_setFrame:frame]; - return; - } - - if (IS_NOTCHED_SCREEN) { - // 竖屏 - if (CGRectGetMinY(frame) == 38) { - // searching - frame = CGRectSetY(frame, 44); - } - - // 横屏 - if (CGRectGetMinY(frame) == -6) { - frame = CGRectSetY(frame, 0); - } - } else { - - // 竖屏 - if (CGRectGetMinY(frame) == 14) { - frame = CGRectSetY(frame, 20); - } - - // 横屏 - if (CGRectGetMinY(frame) == -6) { - frame = CGRectSetY(frame, 0); - } - } - // 强制在激活状态下 高度也为 56,方便后续做平滑过渡动画 (iOS 11 默认下,非刘海屏的机器激活后为 50,刘海屏激活后为 55) - if (frame.size.height == SearchBarActiveHeightIOS11Later) { - frame.size.height = 56; - } - } - - [self qmuisb_setFrame:frame]; -} - - (void)qmui_styledAsQMUISearchBar { if (!QMUICMIActivated) { return; } // 搜索框的字号及 placeholder 的字号 - UIFont *font = SearchBarFont; - if (font) { - self.qmui_font = font; - } + self.qmui_font = SearchBarFont; // 搜索框的文字颜色 - UIColor *textColor = SearchBarTextColor; - if (textColor) { - self.qmui_textColor = textColor; - } + self.qmui_textColor = SearchBarTextColor; // placeholder 的文字颜色 - UIColor *placeholderColor = SearchBarPlaceholderColor; - if (placeholderColor) { - self.qmui_placeholderColor = placeholderColor; - } + self.qmui_placeholderColor = SearchBarPlaceholderColor; self.placeholder = @"搜索"; self.autocorrectionType = UITextAutocorrectionTypeNo; @@ -357,40 +464,440 @@ - (void)qmui_styledAsQMUISearchBar { self.tintColor = SearchBarTintColor; // 输入框背景图 - UIColor *textFieldBackgroundColor = SearchBarTextFieldBackground; - if (textFieldBackgroundColor) { - [self setSearchFieldBackgroundImage:[[UIImage qmui_imageWithColor:textFieldBackgroundColor size:CGSizeMake(60, 28) cornerRadius:0] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)] forState:UIControlStateNormal]; + UIImage *searchFieldBackgroundImage = SearchBarTextFieldBackgroundImage; + if (searchFieldBackgroundImage) { + [self setSearchFieldBackgroundImage:searchFieldBackgroundImage forState:UIControlStateNormal]; } // 输入框边框 UIColor *textFieldBorderColor = SearchBarTextFieldBorderColor; if (textFieldBorderColor) { - self.qmui_textField.layer.borderWidth = PixelOne; - self.qmui_textField.layer.borderColor = textFieldBorderColor.CGColor; + self.searchTextField.layer.borderWidth = PixelOne; + self.searchTextField.layer.borderColor = textFieldBorderColor.CGColor; } // 整条bar的背景 // 为了让 searchBar 底部的边框颜色支持修改,背景色不使用 barTintColor 的方式去改,而是用 backgroundImage + UIImage *backgroundImage = SearchBarBackgroundImage; + if (backgroundImage) { + [self setBackgroundImage:backgroundImage forBarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault]; + [self setBackgroundImage:backgroundImage forBarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefaultPrompt]; + } +} + ++ (UIImage *)qmui_generateTextFieldBackgroundImageWithColor:(UIColor *)color { + // 背景图片的高度会决定输入框的高度,在 iOS 11 及以上,系统默认高度是 36,iOS 10 及以下的高度是 28 的搜索输入框的高度计算:QMUIKit/UIKitExtensions/UISearchBar+QMUI.m + // 至于圆角,输入框会在 UIView 层面控制,背景图里无需处理 + return [[UIImage qmui_imageWithColor:color size:self.qmuisb_textFieldDefaultSize cornerRadius:0] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)]; +} + ++ (UIImage *)qmui_generateBackgroundImageWithColor:(UIColor *)backgroundColor borderColor:(UIColor *)borderColor { UIImage *backgroundImage = nil; + if (backgroundColor || borderColor) { + backgroundImage = [UIImage qmui_imageWithColor:backgroundColor ?: UIColorWhite size:CGSizeMake(10, 10) cornerRadius:0]; + if (borderColor) { + backgroundImage = [backgroundImage qmui_imageWithBorderColor:borderColor borderWidth:PixelOne borderPosition:QMUIImageBorderPositionBottom]; + } + backgroundImage = [backgroundImage resizableImageWithCapInsets:UIEdgeInsetsMake(1, 1, 1, 1)]; + } + return backgroundImage; +} + +#pragma mark - Left Accessory View + +static char kAssociatedObjectKey_showsLeftAccessoryView; +- (void)qmui_setShowsLeftAccessoryView:(BOOL)showsLeftAccessoryView animated:(BOOL)animated { + objc_setAssociatedObject(self, &kAssociatedObjectKey_showsLeftAccessoryView, @(showsLeftAccessoryView), OBJC_ASSOCIATION_RETAIN_NONATOMIC); - UIColor *barTintColor = SearchBarBarTintColor; - if (barTintColor) { - backgroundImage = [UIImage qmui_imageWithColor:barTintColor size:CGSizeMake(10, 10) cornerRadius:0]; + if (animated) { + if (showsLeftAccessoryView) { + self.qmui_leftAccessoryView.hidden = NO; + self.qmui_leftAccessoryView.qmui_frameApplyTransform = CGRectSetXY(self.qmui_leftAccessoryView.frame, -CGRectGetWidth(self.qmui_leftAccessoryView.frame), CGRectGetMinYVerticallyCenter(self.searchTextField.frame, self.qmui_leftAccessoryView.frame)); + [UIView animateWithDuration:.25 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + [self qmuisb_updateCustomTextFieldMargins]; + } completion:nil]; + } else { + [UIView animateWithDuration:.25 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + self.qmui_leftAccessoryView.transform = CGAffineTransformMakeTranslation(-CGRectGetMaxX(self.qmui_leftAccessoryView.frame), 0); + [self qmuisb_updateCustomTextFieldMargins]; + } completion:^(BOOL finished) { + // 快速在 show/hide 之间切换,容易出现状态错误,所以做个保护 + if (showsLeftAccessoryView == self.qmui_showsLeftAccessoryView) { + self.qmui_leftAccessoryView.hidden = YES; + } + self.qmui_leftAccessoryView.transform = CGAffineTransformIdentity; + }]; + } + } else { + self.qmui_leftAccessoryView.hidden = !showsLeftAccessoryView; + [self qmuisb_updateCustomTextFieldMargins]; + } +} + +- (void)setQmui_showsLeftAccessoryView:(BOOL)qmui_showsLeftAccessoryView { + [self qmui_setShowsLeftAccessoryView:qmui_showsLeftAccessoryView animated:NO]; +} + +- (BOOL)qmui_showsLeftAccessoryView { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_showsLeftAccessoryView)) boolValue]; +} + +static char kAssociatedObjectKey_leftAccessoryView; +- (void)setQmui_leftAccessoryView:(UIView *)qmui_leftAccessoryView { + if (self.qmui_leftAccessoryView != qmui_leftAccessoryView) { + [self.qmui_leftAccessoryView removeFromSuperview]; + [self.searchTextField.superview addSubview:qmui_leftAccessoryView]; } + objc_setAssociatedObject(self, &kAssociatedObjectKey_leftAccessoryView, qmui_leftAccessoryView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + qmui_leftAccessoryView.hidden = !self.qmui_showsLeftAccessoryView; + [qmui_leftAccessoryView sizeToFit]; - UIColor *bottomBorderColor = SearchBarBottomBorderColor; - if (bottomBorderColor) { - if (!backgroundImage) { - backgroundImage = [UIImage qmui_imageWithColor:UIColorWhite size:CGSizeMake(10, 10) cornerRadius:0]; + [self qmuisb_updateCustomTextFieldMargins]; +} + +- (UIView *)qmui_leftAccessoryView { + return (UIView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_leftAccessoryView); +} + +static char kAssociatedObjectKey_leftAccessoryViewMargins; +- (void)setQmui_leftAccessoryViewMargins:(UIEdgeInsets)qmui_leftAccessoryViewMargins { + objc_setAssociatedObject(self, &kAssociatedObjectKey_leftAccessoryViewMargins, @(qmui_leftAccessoryViewMargins), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self qmuisb_updateCustomTextFieldMargins]; +} + +- (UIEdgeInsets)qmui_leftAccessoryViewMargins { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_leftAccessoryViewMargins)) UIEdgeInsetsValue]; +} + +// 这个方法会在 textField 调整完布局后才调用,所以可以直接基于 textField 当前的布局去计算布局 +- (void)qmuisb_adjustLeftAccessoryViewFrameAfterTextFieldLayout { + if (self.qmui_leftAccessoryView && !self.qmui_leftAccessoryView.hidden) { + self.qmui_leftAccessoryView.qmui_frameApplyTransform = CGRectSetXY(self.qmui_leftAccessoryView.frame, CGRectGetMinX(self.searchTextField.frame) - [UISearchBar qmuisb_textFieldDefaultMargins].left - self.qmui_leftAccessoryViewMargins.right - CGRectGetWidth(self.qmui_leftAccessoryView.frame), CGRectGetMinYVerticallyCenter(self.searchTextField.frame, self.qmui_leftAccessoryView.frame)); + } +} + +#pragma mark - Right Accessory View + +static char kAssociatedObjectKey_showsRightAccessoryView; +- (void)qmui_setShowsRightAccessoryView:(BOOL)showsRightAccessoryView animated:(BOOL)animated { + objc_setAssociatedObject(self, &kAssociatedObjectKey_showsRightAccessoryView, @(showsRightAccessoryView), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (animated) { + BOOL shouldAnimateAlpha = self.showsCancelButton;// 由于 rightAccessoryView 会从 cancelButton 那边飞过来,会有一点重叠,所以加一个 alpha 过渡 + if (showsRightAccessoryView) { + self.qmui_rightAccessoryView.hidden = NO; + self.qmui_rightAccessoryView.qmui_frameApplyTransform = CGRectSetXY(self.qmui_rightAccessoryView.frame, CGRectGetWidth(self.qmui_rightAccessoryView.superview.bounds), CGRectGetMinYVerticallyCenter(self.searchTextField.frame, self.qmui_rightAccessoryView.frame)); + if (shouldAnimateAlpha) { + self.qmui_rightAccessoryView.alpha = 0; + } + [UIView animateWithDuration:.25 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + [self qmuisb_updateCustomTextFieldMargins]; + if (shouldAnimateAlpha) { + self.qmui_rightAccessoryView.alpha = 1; + } + } completion:nil]; + } else { + [UIView animateWithDuration:.25 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + self.qmui_rightAccessoryView.transform = CGAffineTransformMakeTranslation(CGRectGetWidth(self.qmui_rightAccessoryView.superview.bounds) - CGRectGetMinX(self.qmui_rightAccessoryView.frame), 0); + [self qmuisb_updateCustomTextFieldMargins]; + } completion:^(BOOL finished) { + // 快速在 show/hide 之间切换,容易出现状态错误,所以做个保护 + if (showsRightAccessoryView == self.qmui_showsRightAccessoryView) { + self.qmui_rightAccessoryView.hidden = YES; + } + self.qmui_rightAccessoryView.transform = CGAffineTransformIdentity; + self.qmui_rightAccessoryView.alpha = 1; + }]; + if (shouldAnimateAlpha) { + [UIView animateWithDuration:.18 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{ + self.qmui_rightAccessoryView.alpha = 0; + } completion:nil]; + } } - backgroundImage = [backgroundImage qmui_imageWithBorderColor:bottomBorderColor borderWidth:PixelOne borderPosition:QMUIImageBorderPositionBottom]; + } else { + self.qmui_rightAccessoryView.hidden = !showsRightAccessoryView; + [self qmuisb_updateCustomTextFieldMargins]; } +} + +- (void)setQmui_showsRightAccessoryView:(BOOL)qmui_showsRightAccessoryView { + [self qmui_setShowsRightAccessoryView:qmui_showsRightAccessoryView animated:NO]; +} + +- (BOOL)qmui_showsRightAccessoryView { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_showsRightAccessoryView)) boolValue]; +} + +static char kAssociatedObjectKey_rightAccessoryView; +- (void)setQmui_rightAccessoryView:(UIView *)qmui_rightAccessoryView { + if (self.qmui_rightAccessoryView != qmui_rightAccessoryView) { + [self.qmui_rightAccessoryView removeFromSuperview]; + [self.searchTextField.superview addSubview:qmui_rightAccessoryView]; + } + objc_setAssociatedObject(self, &kAssociatedObjectKey_rightAccessoryView, qmui_rightAccessoryView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (backgroundImage) { - backgroundImage = [backgroundImage resizableImageWithCapInsets:UIEdgeInsetsMake(1, 1, 1, 1)]; - [self setBackgroundImage:backgroundImage forBarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault]; - [self setBackgroundImage:backgroundImage forBarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefaultPrompt]; + qmui_rightAccessoryView.hidden = !self.qmui_showsRightAccessoryView; + [qmui_rightAccessoryView sizeToFit]; + + [self qmuisb_updateCustomTextFieldMargins]; +} + +- (UIView *)qmui_rightAccessoryView { + return (UIView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_rightAccessoryView); +} + +static char kAssociatedObjectKey_rightAccessoryViewMargins; +- (void)setQmui_rightAccessoryViewMargins:(UIEdgeInsets)qmui_rightAccessoryViewMargins { + objc_setAssociatedObject(self, &kAssociatedObjectKey_rightAccessoryViewMargins, @(qmui_rightAccessoryViewMargins), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self qmuisb_updateCustomTextFieldMargins]; +} + +- (UIEdgeInsets)qmui_rightAccessoryViewMargins { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_rightAccessoryViewMargins)) UIEdgeInsetsValue]; +} + +- (void)qmuisb_updateCustomTextFieldMargins { + // 用 qmui_showsLeftAccessoryView 而不是用 !qmui_leftAccessoryView.hidden 是因为做动画时可能 hidden 值还没更新,所以用标志位来区分 + BOOL shouldShowLeftAccessoryView = self.qmui_showsLeftAccessoryView && self.qmui_leftAccessoryView; + BOOL shouldShowRightAccessoryView = self.qmui_showsRightAccessoryView && self.qmui_rightAccessoryView; + CGFloat leftMargin = shouldShowLeftAccessoryView ? CGRectGetWidth(self.qmui_leftAccessoryView.frame) + UIEdgeInsetsGetHorizontalValue(self.qmui_leftAccessoryViewMargins) : 0; + CGFloat rightMargin = shouldShowRightAccessoryView ? CGRectGetWidth(self.qmui_rightAccessoryView.frame) + UIEdgeInsetsGetHorizontalValue(self.qmui_rightAccessoryViewMargins) : 0; + + if (self.qmuisb_customTextFieldMargins.left != leftMargin || self.qmuisb_customTextFieldMargins.right != rightMargin) { + self.qmuisb_customTextFieldMargins = UIEdgeInsetsMake(self.qmuisb_customTextFieldMargins.top, leftMargin, self.qmuisb_customTextFieldMargins.bottom, rightMargin); + [self qmuisb_setNeedsLayoutTextField]; + } +} + +// 这个方法会在 textField 调整完布局后才调用,所以可以直接基于 textField 当前的布局去计算布局 +- (void)qmuisb_adjustRightAccessoryViewFrameAfterTextFieldLayout { + if (self.qmui_rightAccessoryView && !self.qmui_rightAccessoryView.hidden) { + self.qmui_rightAccessoryView.qmui_frameApplyTransform = CGRectSetXY(self.qmui_rightAccessoryView.frame, CGRectGetMaxX(self.searchTextField.frame) + [UISearchBar qmuisb_textFieldDefaultMargins].right + self.qmui_textFieldMargins.right + self.qmui_rightAccessoryViewMargins.left, CGRectGetMinYVerticallyCenter(self.searchTextField.frame, self.qmui_rightAccessoryView.frame)); + } +} + +#pragma mark - Layout + +- (void)qmuisb_setNeedsLayoutTextField { + if (self.searchTextField && !CGRectIsEmpty(self.searchTextField.frame)) { + [self.searchTextField.superview setNeedsLayout]; + [self.searchTextField.superview layoutIfNeeded]; + } +} + +- (BOOL)qmuisb_shouldFixLayoutWhenUsedAsTableHeaderView { + return self.qmui_usedAsTableHeaderView && self.qmui_searchController.hidesNavigationBarDuringPresentation; +} + +- (CGRect)qmuisb_adjustCancelButtonFrame:(CGRect)followingFrame { + if (self.qmuisb_shouldFixLayoutWhenUsedAsTableHeaderView) { + CGRect textFieldFrame = self.searchTextField.frame; + // iOS 13 当 searchBar 作为 tableHeaderView 使用时,并且非搜索状态下 searchBar.showsCancelButton = YES,则进入搜搜状态后再退出,可看到 cancelButton 下降过程中会有抖动 + followingFrame = CGRectSetY(followingFrame, CGRectGetMinYVerticallyCenter(textFieldFrame, followingFrame)); + } + + if (self.qmui_cancelButtonMarginsBlock) { + UIEdgeInsets insets = self.qmui_cancelButtonMarginsBlock(self, self.qmui_isActive); + followingFrame = CGRectInsetEdges(followingFrame, insets); + } + return followingFrame; +} + +- (void)qmuisb_adjustSegmentedControlFrameIfNeeded { + if (!self.qmuisb_shouldFixLayoutWhenUsedAsTableHeaderView) return; + if (self.qmui_isActive) { + CGRect textFieldFrame = self.searchTextField.frame; + if (self.qmui_segmentedControl.superview.qmui_top < self.searchTextField.qmui_bottom) { + // scopeBar 显示在搜索框右边 + self.qmui_segmentedControl.superview.qmui_top = CGRectGetMinYVerticallyCenter(textFieldFrame, self.qmui_segmentedControl.superview.frame); + } + } +} + +- (CGRect)qmuisb_adjustedSearchBarFrameByOriginalFrame:(CGRect)frame { + if (!self.qmuisb_shouldFixLayoutWhenUsedAsTableHeaderView) return frame; + + // 重写 setFrame: 是为了这个 issue:https://github.com/Tencent/QMUI_iOS/issues/233 + // iOS 11 下用 tableHeaderView 的方式使用 searchBar 的话,进入搜索状态时 y 偏上了,导致间距错乱 + // iOS 13 iPad 在退出动画时 y 值可能为负,需要修正 + + if (self.qmui_searchController.isBeingDismissed && CGRectGetMinY(frame) < 0) { + frame = CGRectSetY(frame, 0); + } + + if (!self.qmui_isActive) { + return frame; + } + + if (IS_NOTCHED_SCREEN) { + // 竖屏 + if (CGRectGetMinY(frame) == 38) { + // searching + frame = CGRectSetY(frame, 44); + } + + // 全面屏 iPad + if (CGRectGetMinY(frame) == 18) { + // searching + frame = CGRectSetY(frame, 24); + } + + // 横屏 + if (CGRectGetMinY(frame) == -6) { + frame = CGRectSetY(frame, 0); + } + } else { + + // 竖屏 + if (CGRectGetMinY(frame) == 14) { + frame = CGRectSetY(frame, 20); + } + + // 横屏 + if (CGRectGetMinY(frame) == -6) { + frame = CGRectSetY(frame, 0); + } + } + // 强制在激活状态下 高度也为 56,方便后续做平滑过渡动画 (iOS 11 默认下,非刘海屏的机器激活后为 50,刘海屏激活后为 55) + if (frame.size.height != 56) { + frame.size.height = 56; + } + return frame; +} + +- (CGRect)qmuisb_adjustedSearchTextFieldFrameByOriginalFrame:(CGRect)frame { + if (self.qmuisb_shouldFixLayoutWhenUsedAsTableHeaderView) { + if (@available(iOS 14.0, *)) { + // iOS 14 beta 1 修改了 searchTextField 的 font 属性会导致 TextField 高度异常,临时修复一下 + CGFloat fixedHeight = UISearchBar.qmuisb_textFieldDefaultSize.height; + CGFloat offset = fixedHeight - frame.size.height; + frame.origin.y -= offset / 2.0; + frame.size.height = fixedHeight; + } + if (self.qmui_isActive) { + BOOL statusBarHidden = self.window.windowScene.statusBarManager.statusBarHidden; + CGFloat visibleHeight = statusBarHidden ? 56 : 50; + frame.origin.y = (visibleHeight - self.searchTextField.qmui_height) / 2; + } else if (self.qmui_searchController.isBeingDismissed) { + frame.origin.y = (56 - self.searchTextField.qmui_height) / 2; + } + } + + // apply qmui_textFieldMargins + UIEdgeInsets textFieldMargins = UIEdgeInsetsZero; + if (self.qmui_textFieldMarginsBlock) { + textFieldMargins = self.qmui_textFieldMarginsBlock(self, self.qmui_isActive); + } else { + textFieldMargins = self.qmui_textFieldMargins; + } + if (!UIEdgeInsetsEqualToEdgeInsets(textFieldMargins, UIEdgeInsetsZero)) { + frame = CGRectInsetEdges(frame, textFieldMargins); + } + + if (!UIEdgeInsetsEqualToEdgeInsets(self.qmuisb_customTextFieldMargins, UIEdgeInsetsZero)) { + frame = CGRectInsetEdges(frame, self.qmuisb_customTextFieldMargins); + } + + return frame; +} + +- (void)qmuisb_searchTextFieldFrameDidChange { + // apply SearchBarTextFieldCornerRadius + CGFloat textFieldCornerRadius = SearchBarTextFieldCornerRadius; + if (textFieldCornerRadius != 0) { + textFieldCornerRadius = textFieldCornerRadius > 0 ? textFieldCornerRadius : CGRectGetHeight(self.searchTextField.frame) / 2.0; + } + self.searchTextField.layer.cornerRadius = textFieldCornerRadius; + self.searchTextField.clipsToBounds = textFieldCornerRadius != 0; + + [self qmuisb_adjustLeftAccessoryViewFrameAfterTextFieldLayout]; + [self qmuisb_adjustRightAccessoryViewFrameAfterTextFieldLayout]; + [self qmuisb_adjustSegmentedControlFrameIfNeeded]; +} + +- (void)qmuisb_fixDismissingAnimationIfNeeded { + if (!self.qmuisb_shouldFixLayoutWhenUsedAsTableHeaderView) return; + + if (self.qmui_searchController.isBeingDismissed) { + + if (IS_NOTCHED_SCREEN && self.frame.origin.y == 43) { // 修复刘海屏下,系统计算少了一个 pt + self.frame = CGRectSetY(self.frame, StatusBarHeightConstant); + } + + UIView *searchBarContainerView = self.superview; + // 每次激活搜索框,searchBarContainerView 都会重新创建一个 + if (searchBarContainerView.layer.masksToBounds == YES) { + searchBarContainerView.layer.masksToBounds = NO; + // backgroundView 被 searchBarContainerView masksToBounds 裁减掉的底部。 + CGFloat backgroundViewBottomClipped = CGRectGetMaxY([searchBarContainerView convertRect:self.qmui_backgroundView.frame fromView:self.qmui_backgroundView.superview]) - CGRectGetHeight(searchBarContainerView.bounds); + // UISeachbar 取消激活时,如果 BackgroundView 底部超出了 searchBarContainerView,需要以动画的形式来过渡: + if (backgroundViewBottomClipped > 0) { + CGFloat previousHeight = self.qmui_backgroundView.qmui_height; + [UIView performWithoutAnimation:^{ + // 先减去 backgroundViewBottomClipped 使得 backgroundView 和 searchBarContainerView 底部对齐,由于这个时机是包裹在 animationBlock 里的,所以要包裹在 performWithoutAnimation 中来设置 + self.qmui_backgroundView.qmui_height -= backgroundViewBottomClipped; + }]; + // 再还原高度,这里在 animationBlock 中,所以会以动画来过渡这个效果 + self.qmui_backgroundView.qmui_height = previousHeight; + + // 以下代码为了保持原有的顶部的 mask,否则在 NavigationBar 为透明或者磨砂时,会看到 backgroundView + CAShapeLayer *maskLayer = [CAShapeLayer layer]; + CGMutablePathRef path = CGPathCreateMutable(); + CGPathAddRect(path, NULL, CGRectMake(0, 0, searchBarContainerView.qmui_width, previousHeight)); + maskLayer.path = path; + searchBarContainerView.layer.mask = maskLayer; + CGPathRelease(path); + } + } + } +} + +// UISearchController.searchBar 作为 UITableView.tableHeaderView 时,进入搜索状态,搜索结果列表顶部有一大片空白 +// 不要让系统自适应了,否则在搜索结果(navigationBar 隐藏)push 进入下一级界面(navigationBar 显示)过程中系统自动调整的 contentInset 会跳来跳去 +// https://github.com/Tencent/QMUI_iOS/issues/1473 +- (void)qmuisb_fixSearchResultsScrollViewContentInsetIfNeeded { + if (!self.qmuisb_shouldFixLayoutWhenUsedAsTableHeaderView || !self.qmui_shouldFixSearchResultsContentInset) return; + if (self.qmui_isActive) { + UIViewController *searchResultsController = self.qmui_searchController.searchResultsController; + if (searchResultsController && [searchResultsController isViewLoaded]) { + UIView *view = searchResultsController.view; + UIScrollView *scrollView = + [view isKindOfClass:UIScrollView.class] ? view : + [view.subviews.firstObject isKindOfClass:UIScrollView.class] ? view.subviews.firstObject : nil; + UIView *searchBarContainerView = self.superview; + if (scrollView && scrollView.contentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentNever && searchBarContainerView) { + CGFloat containerHeight = CGRectGetHeight(searchBarContainerView.frame); + scrollView.contentInset = UIEdgeInsetsMake(containerHeight, 0, scrollView.safeAreaInsets.bottom, 0); + scrollView.scrollIndicatorInsets = scrollView.contentInset; + } + } + } +} + +static CGSize textFieldDefaultSize; ++ (CGSize)qmuisb_textFieldDefaultSize { + if (CGSizeIsEmpty(textFieldDefaultSize)) { + // 在 iOS 11 及以上,搜索输入框系统默认高度是 36,iOS 10 及以下的高度是 28 + textFieldDefaultSize = CGSizeMake(60, 36); + } + return textFieldDefaultSize; +} + +// 系统 textField 默认就带有左右间距,也即当 qmui_textFieldMargins 为 0 时输入框与左右的间距,实际计算时要自己叠加上 safeAreaInsets 的值 +static UIEdgeInsets textFieldDefaultMargins; ++ (UIEdgeInsets)qmuisb_textFieldDefaultMargins { + if (UIEdgeInsetsEqualToEdgeInsets(textFieldDefaultMargins, UIEdgeInsetsZero)) { + textFieldDefaultMargins = UIEdgeInsetsMake(10, 8, 10, 8); + } + return textFieldDefaultMargins; +} + +static CGFloat seachBarDefaultActiveHeight; ++ (CGFloat)qmuisb_seachBarDefaultActiveHeight { + if (!seachBarDefaultActiveHeight) { + seachBarDefaultActiveHeight = IS_NOTCHED_SCREEN ? 55 : 50; } + return seachBarDefaultActiveHeight; } @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UISearchController+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UISearchController+QMUI.h new file mode 100644 index 00000000..27a934a8 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UISearchController+QMUI.h @@ -0,0 +1,42 @@ +/** + * 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. + */ +// +// UISearchController+QMUI.h +// QMUIKit +// +// Created by ziezheng on 2019/9/27. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UISearchController (QMUI) + +/// 系统默认是只有搜索框文本不为空时才会显示搜索结果,将该属性置为 YES 可以做到只要 active 就能显示搜索结果列表。 +/// 该属性与 qmui_launchView、obscuresBackgroundDuringPresentation 互斥,打开该属性时会强制清除互斥属性(但如果你非要在打开该属性之后,再重新为这两个互斥属性赋值,也是可以的)。 +/// 默认为 NO。 +@property(nonatomic, assign) BOOL qmui_alwaysShowSearchResultsController; + +/// 当 A 里构造了一个 UISearchController(称为B),当B进入搜索状态后,再 push/present 到其他界面,B的 viewWillAppear: 等生命周期方法并不会被调用,但A的生命周期方法会被调用,这令搜索业务难以感知当前的界面状态。 +/// 若将当前属性置为 YES,则会保证A的生命周期方法被调用时也触发B的生命周期方法。 +/// 默认为 NO。 +@property(nonatomic, assign) BOOL qmui_forwardAppearanceMethodsFromPresentingController; + +/// 升起键盘时的半透明遮罩,nil 表示用系统的,非 nil 则用自己的。默认为 nil。 +/// @note 如果使用了 launchView 则该属性无效。 +@property(nonatomic, strong, nullable) UIColor *qmui_dimmingColor; + +/// 在搜索文字为空时会展示的一个 view,通常用于实现“最近搜索”之类的功能。launchView 最终会被布局为撑满搜索框以下的所有空间。 +@property(nonatomic, strong, nullable) UIView *qmui_launchView; + +/// 获取进入搜索状态后 searchBar 在 UISearchController.view 坐标系内的 maxY 值,方便 searchResultsController 布局。 +@property(nonatomic, assign, readonly) CGFloat qmui_searchBarMaxY; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UISearchController+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UISearchController+QMUI.m new file mode 100644 index 00000000..8b277d63 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UISearchController+QMUI.m @@ -0,0 +1,279 @@ +/** + * 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. + */ +// +// UISearchController+QMUI.m +// QMUIKit +// +// Created by ziezheng on 2019/9/27. +// + +#import "UISearchController+QMUI.h" +#import "QMUICore.h" +#import "UIViewController+QMUI.h" +#import "UINavigationController+QMUI.h" +#import "UIView+QMUI.h" +#import "NSArray+QMUI.h" + +@implementation UISearchController (QMUI) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // -[_UISearchControllerView didMoveToWindow] + // 修复 https://github.com/Tencent/QMUI_iOS/issues/680 中提到的问题二:当有一个 TableViewController A,A 的 seachBar 被激活且 searchResultsController 正在显示的情况下,A.navigationController push 一个新的 viewController B,B 用 pop 手势返回到一半松手放弃返回,此时 B 再 push 一个新的 viewController 时,在转场过程中会看到 searchResultsController 的内容。 + OverrideImplementation(NSClassFromString(@"_UISearchControllerView"), @selector(didMoveToWindow), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject) { + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + if (selfObject.window && [selfObject.superview isKindOfClass:NSClassFromString(@"UITransitionView")]) { + UIView *transitionView = selfObject.superview; + UISearchController *searchController = [selfObject qmui_viewController]; + UIViewController *sourceViewController = [searchController valueForKey:@"_modalSourceViewController"]; + UINavigationController *navigationController = sourceViewController.navigationController; + if (navigationController.qmui_isPushing) { + BOOL isFromPreviousViewController = [sourceViewController qmui_isDescendantOfViewController:navigationController.topViewController.qmui_previousViewController]; + if (!isFromPreviousViewController) { + // 系统内部错误地添加了这个 view,这里直接 remove 掉,系统内部在真正要显示的时候再次添加回来。 + [transitionView removeFromSuperview]; + } + } + } + + }; + }); + + // - [UISearchController viewDidLayoutSubviews] + OverrideImplementation([UISearchController class], @selector(viewDidLayoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISearchController *selfObject) { + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + // 某些场景(比如 setActive:YES animated:NO)会在 _UISearchBarContainerView 被添加到 view 上之后调用 -[UISearchController viewDidLayoutSubviews] 但不会调用 -[searchResultsController viewDidLayoutSubviews],导致搜索结果界面里如果使用 qmui_searchBarMaxY 等依赖于 _UISearchBarContainerView 的方法时就会得到错误结果,所以这里每次都主动刷新搜索结果界面的布局。 + if (selfObject.searchResultsController.isViewLoaded && selfObject.searchResultsController.view.superview.superview == selfObject.view) { + [selfObject.searchResultsController.view setNeedsLayout]; + } + + if (selfObject.qmui_launchView) { + [UIView animateWithDuration:[CATransaction animationDuration] animations:^{ + [selfObject qmuisc_layoutLaunchViewIfNeeded]; + }]; + } + }; + }); + }); +} + +static char kAssociatedObjectKey_alwaysShowSearchResultsController; +- (void)setQmui_alwaysShowSearchResultsController:(BOOL)qmui_alwaysShowSearchResultsController { + BOOL hasSet = !!objc_getAssociatedObject(self, &kAssociatedObjectKey_alwaysShowSearchResultsController); + objc_setAssociatedObject(self, &kAssociatedObjectKey_alwaysShowSearchResultsController, @(qmui_alwaysShowSearchResultsController), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_alwaysShowSearchResultsController) { + self.qmui_launchView = nil; + self.obscuresBackgroundDuringPresentation = NO; + } else if (hasSet) { + // 用变量 hasSet 表示用过 qmui_alwaysShowSearchResultsController 属性再关回去时才需要重置,否则就不用干预 + self.obscuresBackgroundDuringPresentation = YES; + return; + } + [QMUIHelper executeBlock:^{ + // - [UISearchController _updateVisibilityOfSearchResultsForSearchBar:] + // - (void) _updateVisibilityOfSearchResultsForSearchBar:(id)arg1; + OverrideImplementation([UISearchController class], NSSelectorFromString([NSString qmui_stringByConcat:@"_", @"updateVisibility", @"OfSearchResults", @"ForSearchBar:", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISearchController *selfObject, UISearchBar *searchBar) { + + // call super + void (*originSelectorIMP)(id, SEL, UISearchBar *); + originSelectorIMP = (void (*)(id, SEL, UISearchBar *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, searchBar); + + if (selfObject.qmui_alwaysShowSearchResultsController) { + selfObject.searchResultsController.view.hidden = NO; + } + }; + }); + } oncePerIdentifier:@"UISearchController (QMUI) alwaysShowResults"]; +} + +- (BOOL)qmui_alwaysShowSearchResultsController { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_alwaysShowSearchResultsController)) boolValue]; +} + +static char kAssociatedObjectKey_forwardAppearance; +- (void)setQmui_forwardAppearanceMethodsFromPresentingController:(BOOL)qmui_forwardAppearanceMethodsFromPresentingController { + objc_setAssociatedObject(self, &kAssociatedObjectKey_forwardAppearance, @(qmui_forwardAppearanceMethodsFromPresentingController), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_forwardAppearanceMethodsFromPresentingController) { + [QMUIHelper executeBlock:^{ + OverrideImplementation([UIViewController class], @selector(viewWillAppear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISearchController *selfObject, BOOL firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + UISearchController *searchController = [selfObject.presentedViewController isKindOfClass:UISearchController.class] ? (UISearchController *)selfObject.presentedViewController : nil; + if (searchController && searchController.qmui_forwardAppearanceMethodsFromPresentingController && searchController.active) { + [searchController beginAppearanceTransition:YES animated:firstArgv]; + } + }; + }); + + OverrideImplementation([UIViewController class], @selector(viewDidAppear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISearchController *selfObject, BOOL firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + UISearchController *searchController = [selfObject.presentedViewController isKindOfClass:UISearchController.class] ? (UISearchController *)selfObject.presentedViewController : nil; + if (searchController && searchController.qmui_forwardAppearanceMethodsFromPresentingController && searchController.active) { + [searchController endAppearanceTransition]; + } + }; + }); + + OverrideImplementation([UIViewController class], @selector(viewWillDisappear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISearchController *selfObject, BOOL firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + UISearchController *searchController = [selfObject.presentedViewController isKindOfClass:UISearchController.class] ? (UISearchController *)selfObject.presentedViewController : nil; + if (searchController && searchController.qmui_forwardAppearanceMethodsFromPresentingController && searchController.active) { + [searchController beginAppearanceTransition:NO animated:firstArgv]; + } + }; + }); + + OverrideImplementation([UIViewController class], @selector(viewDidDisappear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISearchController *selfObject, BOOL firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + UISearchController *searchController = [selfObject.presentedViewController isKindOfClass:UISearchController.class] ? (UISearchController *)selfObject.presentedViewController : nil; + if (searchController && searchController.qmui_forwardAppearanceMethodsFromPresentingController && searchController.active) { + [searchController endAppearanceTransition]; + } + }; + }); + } oncePerIdentifier:@"UISearchController (QMUI) forwardAppearance"]; + } +} + +- (BOOL)qmui_forwardAppearanceMethodsFromPresentingController { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_forwardAppearance)) boolValue]; +} + +- (CGFloat)qmui_searchBarMaxY { + if (!self.viewLoaded) return 0; + + UIView *searchBarContainerView = [self.view.subviews qmui_firstMatchWithBlock:^BOOL(__kindof UIView * _Nonnull subview) { + return [NSStringFromClass(subview.class) isEqualToString:@"_UISearchBarContainerView"]; + }]; + CGFloat maxY = searchBarContainerView ? CGRectGetMaxY(searchBarContainerView.frame) : 0; + return maxY; +} + +static char kAssociatedObjectKey_dimmingColor; +- (void)setQmui_dimmingColor:(UIColor *)qmui_dimmingColor { + objc_setAssociatedObject(self, &kAssociatedObjectKey_dimmingColor, qmui_dimmingColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [QMUIHelper executeBlock:^{ + // - [UIDimmingView updateBackgroundColor] + OverrideImplementation(NSClassFromString([NSString qmui_stringByConcat:@"UI", @"Dimming", @"View", nil]), NSSelectorFromString([NSString qmui_stringByConcat:@"update", @"Background", @"Color", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject) { + for (UIView *subview in selfObject.superview.subviews) { + // _UISearchControllerView + if ([NSStringFromClass(subview.class) isEqualToString:[NSString qmui_stringByConcat:@"_", @"UISearchController", @"View", nil]]) { + UISearchController *searchController = subview.qmui_viewController; + if ([searchController isKindOfClass:UISearchController.class]) { + UIColor *color = searchController.qmui_dimmingColor; + if (color) { + // - [UIDimmingView setDimmingColor:] + [selfObject qmui_performSelector:NSSelectorFromString(@"setDimmingColor:") withArguments:&color, nil]; + } + } else { + QMUIAssert(NO, @"UISearchController (QMUI)", @"qmui_dimmingColor 找到的 vc 类型错误"); + } + break; + } + } + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + }; + }); + } oncePerIdentifier:@"QMUISearchController dimmingColor"]; +} + +- (UIColor *)qmui_dimmingColor { + return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_dimmingColor); +} + +static char kAssociatedObjectKey_launchView; +- (void)setQmui_launchView:(UIView *)qmui_launchView { + if (self.qmui_launchView != qmui_launchView) { + [self.qmui_launchView removeFromSuperview]; + } + objc_setAssociatedObject(self, &kAssociatedObjectKey_launchView, qmui_launchView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + if (qmui_launchView) { + [QMUIHelper executeBlock:^{ + // - [UISearchController viewWillAppear:] + OverrideImplementation([UISearchController class], @selector(viewWillAppear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISearchController *selfObject, BOOL firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + [selfObject qmuisc_addLaunchViewIfNeeded]; + }; + }); + } oncePerIdentifier:@"UISearchController (QMUI) launchView"]; + } + + self.obscuresBackgroundDuringPresentation = !qmui_launchView; + if (self.viewLoaded) { + [self qmuisc_addLaunchViewIfNeeded]; + } +} + +- (UIView *)qmui_launchView { + return (UIView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_launchView); +} + +- (void)qmuisc_addLaunchViewIfNeeded { + if (!self.qmui_launchView) return; + UIView *superviewOfLaunchView = self.searchResultsController.view.superview; + if (self.qmui_launchView.superview != superviewOfLaunchView) { + [superviewOfLaunchView insertSubview:self.qmui_launchView atIndex:0]; + [self qmuisc_layoutLaunchViewIfNeeded]; + } +} + +- (void)qmuisc_layoutLaunchViewIfNeeded { + if (!self.qmui_launchView || !self.viewLoaded) return; + self.qmui_launchView.frame = CGRectInsetEdges(self.qmui_launchView.superview.bounds, UIEdgeInsetsMake(self.qmui_searchBarMaxY, 0, 0, 0)); +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UISlider+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UISlider+QMUI.h new file mode 100644 index 00000000..8d825812 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UISlider+QMUI.h @@ -0,0 +1,62 @@ +/** + * 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. + */ +// +// UISlider+QMUI.h +// QMUIKit +// +// Created by MoLice on 2021/D/10. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QMUISliderStepControl; + +@interface UISlider (QMUI) + +/// 中间的圆球的 view(类型为 UIImageView) +@property(nullable, nonatomic, strong, readonly) UIView *qmui_thumbView; + +/// 背后导轨的高度,默认为 0,表示使用系统默认的高度。 +@property(nonatomic, assign) IBInspectable CGFloat qmui_trackHeight UI_APPEARANCE_SELECTOR; + +/// 中间圆球的大小,默认为 CGSizeZero +/// @warning 注意若设置了 thumbSize 但没设置 thumbColor,则圆点的颜色会使用 self.tintColor 的颜色(而系统 UISlider 默认的圆点颜色是白色带阴影,不跟 tintColor 走) +@property(nonatomic, assign) IBInspectable CGSize qmui_thumbSize UI_APPEARANCE_SELECTOR; + +/// 中间圆球的颜色,仅当设置了 qmui_thumbSize 时才有效。默认为 nil,nil 表示用 self.tintColor。 +/// @warning 注意在使用了 qmui_thumbSize 时请勿使用系统的 thumbTintColor,后者会导致 qmui_thumbSize 无效。 +@property(nullable, nonatomic, strong) IBInspectable UIColor *qmui_thumbColor UI_APPEARANCE_SELECTOR; + +/// 中间圆球的阴影样式,默认为 nil,也即没有阴影。 +@property(nullable, nonatomic, strong) NSShadow *qmui_thumbShadow UI_APPEARANCE_SELECTOR; + +/// 用于实现只有若干个离散数值的 slider 交互,该属性可控制圆点停靠的位置数量,默认为0,当设置为大于等于2的值时才启用该交互模式。 +@property(nonatomic, assign) NSUInteger qmui_numberOfSteps; + +/// 当使用了 step 功能时,可通过这个属性设置当前在第几档,或者获取当前的值。 +@property(nonatomic, assign) NSUInteger qmui_step; + +/// 在设置 qmui_numberOfSteps 时会创建对应个数的 QMUISliderStepControl,而通过这个 configuration block 可以配置每一个 stepControl 的属性 +@property(nullable, nonatomic, copy) void (^qmui_stepControlConfiguration)(__kindof UISlider *slider, QMUISliderStepControl *stepControl, NSUInteger index); + +/// 当使用了 step 功能时,可通过这个 block 监听 step 的变化(只有 step 的值改变时才会触发),获取当前 step 的值请调用 slider.qmui_step,获取变化前的 step 值请访问参数 precedingStep。 +/// @note 在系统的 UIControlEventValueChanged 里获取 slider.qmui_step 也可以,但因为 slider.continuous 默认是 YES,所以拖动过程中 UIControlEventValueChanged 会触发很多次,但 step 不一定有变化,所以用专门的 block 监听会更方便高效一点。 +@property(nullable, nonatomic, copy) void (^qmui_stepDidChangeBlock)(__kindof UISlider *slider, NSUInteger precedingStep); +@end + +@interface QMUISliderStepControl : UIControl + +@property(nonatomic, strong, readonly) UILabel *titleLabel; +@property(nonatomic, strong, readonly) UIView *indicator; +@property(nonatomic, assign) CGSize indicatorSize UI_APPEARANCE_SELECTOR; +@property(nonatomic, assign) CGFloat spacingBetweenTitleAndIndicator UI_APPEARANCE_SELECTOR; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UISlider+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UISlider+QMUI.m new file mode 100644 index 00000000..70747401 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UISlider+QMUI.m @@ -0,0 +1,476 @@ +/** + * 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. + */ +// +// UISlider+QMUI.m +// QMUIKit +// +// Created by MoLice on 2021/D/10. +// + +#import "UISlider+QMUI.h" +#import "QMUICore.h" +#import "NSNumber+QMUI.h" +#import "UIImage+QMUI.h" +#import "UIView+QMUI.h" +#import "UILabel+QMUI.h" +#import "CALayer+QMUI.h" +#import "NSShadow+QMUI.h" + +@interface UISlider () + +@property(nonatomic, strong) NSMutableArray *qmuisl_stepControls; +@property(nonatomic, copy) NSString *qmuisl_layoutCachedKey; +@property(nonatomic, assign) NSUInteger qmuisl_precedingStep; +@end + +@implementation UISlider (QMUI) + +QMUISynthesizeIdStrongProperty(qmuisl_stepControls, setQmuisl_stepControls) +QMUISynthesizeIdCopyProperty(qmuisl_layoutCachedKey, setQmuisl_layoutCachedKey) +QMUISynthesizeNSUIntegerProperty(qmuisl_precedingStep, setQmuisl_precedingStep) +QMUISynthesizeIdCopyProperty(qmui_stepDidChangeBlock, setQmui_stepDidChangeBlock) + +- (UIView *)qmui_thumbView { + // thumbView 并非在一开始就存在,而是在某个时机才生成的。如果使用了自己的 thumbImage,则系统用 _thumbView 来显示。如果没用自己的 thumbImage,则系统用 _innerThumbView 来存放。注意如果是 _innerThumbView,它外部还有一个 _thumbViewNeue 用来控制布局。 + UIView *slider = self; + if (@available(iOS 14.0, *)) { + slider = [self qmui_valueForKey:@"_visualElement"]; + } + if (!slider) return nil; + + UIView *thumbView = [slider qmui_valueForKey:@"thumbView"] ?: [slider qmui_valueForKey:@"innerThumbView"]; + return thumbView; +} + +static char kAssociatedObjectKey_trackHeight; +- (void)setQmui_trackHeight:(CGFloat)trackHeight { + objc_setAssociatedObject(self, &kAssociatedObjectKey_trackHeight, @(trackHeight), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (trackHeight <= 0) return; + + [QMUIHelper executeBlock:^{ + OverrideImplementation([UISlider class], @selector(trackRectForBounds:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGRect(UISlider *selfObject, CGRect bounds) { + + // call super + CGRect (*originSelectorIMP)(id, SEL, CGRect); + originSelectorIMP = (CGRect (*)(id, SEL, CGRect))originalIMPProvider(); + CGRect result = originSelectorIMP(selfObject, originCMD, bounds); + + if (selfObject.qmui_trackHeight > 0) { + result = CGRectSetHeight(result, selfObject.qmui_trackHeight); + result = CGRectSetY(result, CGFloatGetCenter(CGRectGetHeight(bounds), CGRectGetHeight(result))); + } + + return result; + }; + }); + } oncePerIdentifier:@"UISlider (QMUI) trackHeight"]; + + [self setNeedsLayout]; +} + +- (CGFloat)qmui_trackHeight { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_trackHeight)) qmui_CGFloatValue]; +} + +static char kAssociatedObjectKey_thumbSize; +- (void)setQmui_thumbSize:(CGSize)thumbSize { + objc_setAssociatedObject(self, &kAssociatedObjectKey_thumbSize, @(thumbSize), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (CGSizeIsEmpty(thumbSize)) return; + [self qmuisl_updateThumbImage]; +} + +- (CGSize)qmui_thumbSize { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_thumbSize)) CGSizeValue]; +} + +static char kAssociatedObjectKey_thumbColor; +- (void)setQmui_thumbColor:(UIColor *)thumbColor { + objc_setAssociatedObject(self, &kAssociatedObjectKey_thumbColor, thumbColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self qmuisl_updateThumbImage]; +} + +- (UIColor *)qmui_thumbColor { + return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_thumbColor); +} + +- (void)qmuisl_updateThumbImage { + if (!CGSizeIsEmpty(self.qmui_thumbSize)) { + UIColor *thumbColor = self.qmui_thumbColor ?: self.tintColor; + UIImage *thumbImage = [UIImage qmui_imageWithShape:QMUIImageShapeOval size:self.qmui_thumbSize tintColor:thumbColor]; + [self setThumbImage:thumbImage forState:UIControlStateNormal]; + [self setThumbImage:thumbImage forState:UIControlStateHighlighted]; + } +} + +static char kAssociatedObjectKey_thumbShadow; +- (void)setQmui_thumbShadow:(NSShadow *)thumbShadow { + objc_setAssociatedObject(self, &kAssociatedObjectKey_thumbShadow, thumbShadow, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (thumbShadow) { + [QMUIHelper executeBlock:^{ + if (@available(iOS 14.0, *)) { + // -[_UISlideriOSVisualElement didAddSubview:] + OverrideImplementation(NSClassFromString([NSString qmui_stringByConcat:@"_", @"UISlider", @"iOS", @"VisualElement", nil]), @selector(didAddSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, UIView *subview) { + + // call super + void (*originSelectorIMP)(id, SEL, UIView *); + originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, subview); + + UISlider *slider = (UISlider *)selfObject.superview; + if (![slider isKindOfClass:UISlider.class]) return; + UIView *tv = slider.qmui_thumbView; + if (tv) { + tv.layer.qmui_shadow = slider.qmui_thumbShadow; + } + }; + }); + } else { + OverrideImplementation([UISlider class], @selector(didAddSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISlider *selfObject, UIView *subview) { + + // call super + void (*originSelectorIMP)(id, SEL, UIView *); + originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, subview); + + UIView *tv = selfObject.qmui_thumbView; + if (tv) { + tv.layer.qmui_shadow = selfObject.qmui_thumbShadow; + } + }; + }); + } + } oncePerIdentifier:@"UISlider (QMUI) thumbShadow"]; + } + UIView *thumbView = self.qmui_thumbView; + if (thumbView) { + thumbView.layer.qmui_shadow = thumbShadow; + } +} + +- (NSShadow *)qmui_thumbShadow { + return (NSShadow *)objc_getAssociatedObject(self, &kAssociatedObjectKey_thumbShadow); +} + +#pragma mark - Steps + +static char kAssociatedObjectKey_numberOfSteps; +- (void)setQmui_numberOfSteps:(NSUInteger)numberOfSteps { + objc_setAssociatedObject(self, &kAssociatedObjectKey_numberOfSteps, @(numberOfSteps), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (numberOfSteps < 2) { + [self.qmuisl_stepControls enumerateObjectsUsingBlock:^(QMUISliderStepControl * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [obj removeFromSuperview]; + }]; + self.qmuisl_stepControls = nil; + [self removeTarget:self action:@selector(qmuisl_handleValueChanged:) forControlEvents:UIControlEventValueChanged]; + return; + } + + [self qmuisl_swizzleForStepsIfNeeded]; + + // step 的逻辑都是基于 [0, 1] 来计算的,所以这里强制保证一下值 + self.minimumValue = 0; + self.maximumValue = 1; + + if (!self.qmuisl_stepControls) { + self.qmuisl_stepControls = NSMutableArray.new; + } + NSInteger diff = self.qmuisl_stepControls.count - numberOfSteps; + if (diff < 0) { + for (NSInteger i = 0; i < diff * -1; i++) { + QMUISliderStepControl *stepControl = QMUISliderStepControl.new; + [stepControl addTarget:self action:@selector(qmuisl_handleStepControlEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:stepControl];// stepControl 要在最前面,才能做到点击 stepControl 时响应到点击事件 + [self.qmuisl_stepControls addObject:stepControl]; + } + } else if (diff > 0) { + for (NSInteger i = self.qmuisl_stepControls.count - 1, l = self.qmuisl_stepControls.count - diff - 1; i >= l; i--) { + [self.qmuisl_stepControls[i] removeFromSuperview]; + [self.qmuisl_stepControls removeObjectAtIndex:i]; + } + } + if (self.qmui_stepControlConfiguration) { + [self.qmuisl_stepControls enumerateObjectsUsingBlock:^(QMUISliderStepControl * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + self.qmui_stepControlConfiguration(self, obj, idx); + }]; + } + [self qmuisl_setNeedsLayout]; + + [self removeTarget:self action:@selector(qmuisl_handleValueChanged:) forControlEvents:UIControlEventValueChanged]; + [self addTarget:self action:@selector(qmuisl_handleValueChanged:) forControlEvents:UIControlEventValueChanged]; +} + +- (NSUInteger)qmui_numberOfSteps { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_numberOfSteps)) unsignedIntValue]; +} + +- (void)setQmui_step:(NSUInteger)step { + if (self.qmui_numberOfSteps < 2) return; + CGFloat value = (self.maximumValue - self.minimumValue) * ((CGFloat)step / (CGFloat)(self.qmui_numberOfSteps - 1)); + self.value = value; + [self sendActionsForControlEvents:UIControlEventValueChanged]; +} + +- (NSUInteger)qmui_step { + NSUInteger step = [self qmuisl_stepWithValue:self.value]; + return step; +} + +static char kAssociatedObjectKey_stepControlConfiguration; +- (void)setQmui_stepControlConfiguration:(void (^)(__kindof UISlider * _Nonnull, QMUISliderStepControl * _Nonnull, NSUInteger))stepControlConfiguration { + objc_setAssociatedObject(self, &kAssociatedObjectKey_stepControlConfiguration, stepControlConfiguration, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (stepControlConfiguration) { + [self.qmuisl_stepControls enumerateObjectsUsingBlock:^(QMUISliderStepControl * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + stepControlConfiguration(self, obj, idx); + }]; + [self qmuisl_setNeedsLayout]; + } +} + +- (void (^)(__kindof UISlider * _Nonnull, QMUISliderStepControl * _Nonnull, NSUInteger))qmui_stepControlConfiguration { + return (void (^)(UISlider * _Nonnull, QMUISliderStepControl * _Nonnull, NSUInteger))objc_getAssociatedObject(self, &kAssociatedObjectKey_stepControlConfiguration); +} + +- (void)qmuisl_handleValueChanged:(UISlider *)slider { + if (slider.qmui_numberOfSteps < 2) return; + + NSUInteger step = [slider qmuisl_stepWithValue:slider.value]; + if (step != slider.qmuisl_precedingStep) { + if (slider.qmui_stepDidChangeBlock) { + slider.qmui_stepDidChangeBlock(slider, slider.qmuisl_precedingStep); + } + // 即便不存在 qmui_stepDidChangeBlock 也要记录 precedingStep + // https://github.com/Tencent/QMUI_iOS/issues/1413 + slider.qmuisl_precedingStep = step; + } +} + +- (void)qmuisl_handleStepControlEvent:(QMUISliderStepControl *)stepControl { + NSInteger step = [self.qmuisl_stepControls indexOfObject:stepControl]; + self.qmui_step = step; +} + +- (NSUInteger)qmuisl_stepWithValue:(float)value { + CGFloat progress = value / (self.maximumValue - self.minimumValue); + NSUInteger step = round(progress * (self.qmui_numberOfSteps - 1)); + return step; +} + +- (void)qmuisl_swizzleForStepsIfNeeded { + [QMUIHelper executeBlock:^{ + OverrideImplementation([UISlider class], @selector(layoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISlider *selfObject) { + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + [selfObject qmuisl_layoutStepControls]; + }; + }); + + OverrideImplementation([UISlider class], @selector(setEnabled:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISlider *selfObject, BOOL enabled) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, enabled); + + if (selfObject.qmui_stepControlConfiguration) { + [selfObject.qmuisl_stepControls enumerateObjectsUsingBlock:^(QMUISliderStepControl * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + selfObject.qmui_stepControlConfiguration(selfObject, obj, idx); + }]; + } + }; + }); + + OverrideImplementation([UISlider class], @selector(tintColorDidChange), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISlider *selfObject) { + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + [selfObject qmuisl_tintColorDidChange]; + }; + }); + + OverrideImplementation([UISlider class], @selector(pointInside:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^BOOL(UISlider *selfObject, CGPoint point, UIEvent *event) { + + // call super + BOOL (*originSelectorIMP)(id, SEL, CGPoint, UIEvent *); + originSelectorIMP = (BOOL (*)(id, SEL, CGPoint, UIEvent *))originalIMPProvider(); + BOOL result = originSelectorIMP(selfObject, originCMD, point, event); + + if (!result && selfObject.qmuisl_stepControls.count) { + __block BOOL pointInStepControl = NO; + [selfObject.qmuisl_stepControls enumerateObjectsUsingBlock:^(QMUISliderStepControl * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + CGPoint p = [selfObject convertPoint:point toView:obj]; + if ([obj pointInside:p withEvent:event]) { + pointInStepControl = YES; + *stop = YES; + } + }]; + if (pointInStepControl) return YES; + } + + return result; + }; + }); + + // - (CGRect)thumbRectForBounds:(CGRect)bounds trackRect:(CGRect)rect value:(float)value; + OverrideImplementation([UISlider class], @selector(thumbRectForBounds:trackRect:value:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGRect(UISlider *selfObject, CGRect bounds, CGRect trackRect, float value) { + + // call super + CGRect (*originSelectorIMP)(id, SEL, CGRect, CGRect, float); + originSelectorIMP = (CGRect (*)(id, SEL, CGRect, CGRect, float))originalIMPProvider(); + CGRect result = originSelectorIMP(selfObject, originCMD, bounds, trackRect, value); + + if (selfObject.qmui_numberOfSteps >= 2) { + NSInteger step = [selfObject qmuisl_stepWithValue:value]; + CGFloat thumbCenterX = CGRectGetMinX(trackRect) + (CGRectGetWidth(trackRect) / (selfObject.qmui_numberOfSteps - 1)) * step; + result = CGRectSetX(result, thumbCenterX - CGRectGetWidth(result) / 2); + return result; + } + + return result; + }; + }); + + OverrideImplementation([UISlider class], @selector(setValue:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISlider *selfObject, float value, BOOL animated) { + + // 关闭 continuous 本质上只是让系统在 touch 结束时才 send value changed event,实际上不管 continuous 的值是什么,拖动过程中都会不断调用 setValue:animated: 并且实时设置当前的 value,所以需要重写这个方法,在抬手时强制把当前抬手位置的 value 转换成 UI 上 thumView 当前位置对应的 value 值,然后业务才能在 value changed 回调里获取到正确的 value(虽然业务应该获取 step 而不是 value) + if (selfObject.qmui_numberOfSteps >= 2) { + NSUInteger step = [selfObject qmuisl_stepWithValue:value]; + value = (float)step / (selfObject.qmui_numberOfSteps - 1); + } + + // call super + void (*originSelectorIMP)(id, SEL, float, BOOL); + originSelectorIMP = (void (*)(id, SEL, float, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, value, animated); + + + }; + }); + } oncePerIdentifier:@"UISlider (QMUI) stepControl"]; +} + +- (void)qmuisl_layoutStepControls { + NSInteger count = self.qmuisl_stepControls.count; + if (!count) return; + + // 根据当前 thumbView 的位置,控制重叠的那个 stepControl 的事件响应和显隐,由于 slider 可能是 continuous 的,所以这段逻辑必须每次 layout 都调用,不能放在 layoutCachedKey 的保护里 + CGRect thumbRect = self.qmui_thumbView.frame; + CGRect trackRect = [self trackRectForBounds:self.bounds]; + NSUInteger step = round((CGRectGetMidX(thumbRect) - CGRectGetMinX(trackRect)) / CGRectGetWidth(trackRect) * (count - 1)); + [self.qmuisl_stepControls enumerateObjectsUsingBlock:^(QMUISliderStepControl * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + obj.userInteractionEnabled = idx != step;// 让 stepControl 不要影响 thumbView 的事件 + obj.indicator.hidden = idx == step; + }]; + + NSString *layoutCachedKey = [NSString stringWithFormat:@"%.0f-%@", CGRectGetWidth(trackRect), @(count)]; + if ([self.qmuisl_layoutCachedKey isEqualToString:layoutCachedKey]) return; + + __block CGFloat totalStepsWidth = 0; + [self.qmuisl_stepControls enumerateObjectsUsingBlock:^(QMUISliderStepControl * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + totalStepsWidth += obj.indicatorSize.width; + }]; + CGFloat stepMargin = (CGRectGetWidth(trackRect) - totalStepsWidth) / (count - 1); + __block CGFloat stepIndicatorMinX = CGRectGetMinX(trackRect); + [self.qmuisl_stepControls enumerateObjectsUsingBlock:^(QMUISliderStepControl * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (idx == count - 1) { + // 因为布局过程中可能存在一些像素不对齐的情况,因此对最后一个 indicator 做保护,一定贴着 slider 的 maxX + stepIndicatorMinX = CGRectGetMaxX(trackRect) - obj.indicatorSize.width; + } + CGRect indicatorFrame = CGRectFlatMake(stepIndicatorMinX, CGRectGetMinY(trackRect) + CGFloatGetCenter(CGRectGetHeight(trackRect), obj.indicatorSize.height), obj.indicatorSize.width, obj.indicatorSize.height); + stepIndicatorMinX = CGRectGetMaxX(indicatorFrame) + stepMargin; + + CGSize stepControlSize = [obj sizeThatFits:CGSizeMax]; + obj.frame = CGRectFlatMake(CGRectGetMinX(indicatorFrame) - (stepControlSize.width - CGRectGetWidth(indicatorFrame)) / 2, CGRectGetMaxY(indicatorFrame) - stepControlSize.height, stepControlSize.width, stepControlSize.height); + }]; +} + +- (void)qmuisl_tintColorDidChange { + NSInteger count = self.qmuisl_stepControls.count; + if (!count) return; + [self.qmuisl_stepControls enumerateObjectsUsingBlock:^(QMUISliderStepControl * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + obj.tintColor = self.tintColor; + }]; +} + +- (void)qmuisl_setNeedsLayout { + self.qmuisl_layoutCachedKey = nil; + [self setNeedsLayout]; +} + +@end + +@implementation QMUISliderStepControl + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + _titleLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(12) textColor:UIColorGray]; + self.titleLabel.userInteractionEnabled = NO; + [self addSubview:self.titleLabel]; + + _indicator = [[UIView alloc] init]; + self.indicator.userInteractionEnabled = NO; + self.indicator.backgroundColor = UIColorGray; + [self addSubview:self.indicator]; + + self.indicatorSize = CGSizeMake(1, 8); + self.spacingBetweenTitleAndIndicator = 8; + + // 避免只显示 indicator 时 size 太小,很难点到 + self.qmui_outsideEdge = UIEdgeInsetsMake(-12, -12, -12, -12); + } + return self; +} + +- (void)setIndicatorSize:(CGSize)indicatorSize { + _indicatorSize = indicatorSize; + [((UISlider *)self.superview) qmuisl_setNeedsLayout]; +} + +- (void)setSpacingBetweenTitleAndIndicator:(CGFloat)spacingBetweenTitleAndIndicator { + _spacingBetweenTitleAndIndicator = spacingBetweenTitleAndIndicator; + [((UISlider *)self.superview) qmuisl_setNeedsLayout]; +} + +- (CGSize)sizeThatFits:(CGSize)size { + CGSize titleLabelSize = self.titleLabel.text.length ? [self.titleLabel sizeThatFits:CGSizeMax] : CGSizeZero; + if (CGSizeIsEmpty(titleLabelSize)) return self.indicatorSize; + + CGSize result = CGSizeZero; + result.width = MAX(titleLabelSize.width, self.indicatorSize.width); + result.height = titleLabelSize.height + self.spacingBetweenTitleAndIndicator + self.indicatorSize.height; + return result; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + CGSize titleLabelSize = self.titleLabel.text.length ? [self.titleLabel sizeThatFits:CGSizeMax] : CGSizeZero; + if (CGSizeIsEmpty(titleLabelSize)) { + self.indicator.frame = CGRectMakeWithSize(self.indicatorSize); + } else { + self.titleLabel.frame = CGRectFlatMake(CGFloatGetCenter(CGRectGetWidth(self.bounds), titleLabelSize.width), 0, titleLabelSize.width, titleLabelSize.height); + self.indicator.frame = CGRectFlatMake(CGFloatGetCenter(CGRectGetWidth(self.bounds), self.indicatorSize.width), CGRectGetMaxY(self.titleLabel.frame) + self.spacingBetweenTitleAndIndicator, self.indicatorSize.width, self.indicatorSize.height); + } +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UISwitch+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UISwitch+QMUI.h new file mode 100644 index 00000000..d0163098 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UISwitch+QMUI.h @@ -0,0 +1,26 @@ +/** + * 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. + */ +// +// UISwitch+QMUI.h +// QMUIKit +// +// Created by MoLice on 2019/7/12. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UISwitch (QMUI) + +/// 用于设置 UISwitch 关闭时的背景色(除了圆点外的其他颜色) +@property(nonatomic, strong) UIColor *qmui_offTintColor; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UISwitch+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UISwitch+QMUI.m new file mode 100644 index 00000000..4f2fe7af --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UISwitch+QMUI.m @@ -0,0 +1,81 @@ +/** + * 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. + */ +// +// UISwitch+QMUI.m +// QMUIKit +// +// Created by MoLice on 2019/7/12. +// + +#import "UISwitch+QMUI.h" +#import "QMUICore.h" + +@implementation UISwitch (QMUI) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ExtendImplementationOfNonVoidMethodWithSingleArgument([UISwitch class], @selector(initWithFrame:), CGRect, UISwitch *, ^UISwitch *(UISwitch *selfObject, CGRect firstArgv, UISwitch *originReturnValue) { + if (QMUICMIActivated) { + if (SwitchOffTintColor) { + selfObject.qmui_offTintColor = SwitchOffTintColor; + } + } + return originReturnValue; + }); + + // 设置 qmui_offTintColor 的原理是找到 UISwitch 内部的 switchWellView 并改变它的 backgroundColor,而 switchWellView 在某些时机会重新创建 ,因此需要在这些时机之后对 switchWellView 重新设置一次背景颜色: + OverrideImplementation([UISwitch class], @selector(traitCollectionDidChange:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UISwitch *selfObject, UITraitCollection *previousTraitCollection) { + + // call super + void (*originSelectorIMP)(id, SEL, UITraitCollection *); + originSelectorIMP = (void (*)(id, SEL, UITraitCollection *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, previousTraitCollection); + + BOOL interfaceStyleChanged = [previousTraitCollection hasDifferentColorAppearanceComparedToTraitCollection:selfObject.traitCollection]; + if (interfaceStyleChanged) { + // 在 iOS 13 切换 Dark/Light Mode 之后,会在重新创建 switchWellView,之所以延迟一个 runloop 是因为这个时机是在晚于 traitCollectionDidChange 的 _traitCollectionDidChangeInternal中进行 + dispatch_async(dispatch_get_main_queue(), ^{ + [selfObject qmui_applyOffTintColorIfNeeded]; + }); + } + }; + }); + }); +} + + +static char kAssociatedObjectKey_offTintColor; +static NSString * const kDefaultOffTintColorKey = @"defaultOffTintColorKey"; + +- (void)setQmui_offTintColor:(UIColor *)qmui_offTintColor { + UIView *switchWellView = [self valueForKeyPath:@"_visualElement._switchWellView"]; + UIColor *defaultOffTintColor = [switchWellView qmui_getBoundObjectForKey:kDefaultOffTintColorKey]; + if (!defaultOffTintColor) { + defaultOffTintColor = switchWellView.backgroundColor; + [switchWellView qmui_bindObject:defaultOffTintColor forKey:kDefaultOffTintColorKey]; + } + // 当 offTintColor 为 nil 时,恢复默认颜色(和 setOnTintColor 行为保持一致) + switchWellView.backgroundColor = qmui_offTintColor ? : defaultOffTintColor; + objc_setAssociatedObject(self, &kAssociatedObjectKey_offTintColor, qmui_offTintColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (UIColor *)qmui_offTintColor { + return objc_getAssociatedObject(self, &kAssociatedObjectKey_offTintColor); +} + +- (void)qmui_applyOffTintColorIfNeeded { + if (self.qmui_offTintColor) { + self.qmui_offTintColor = self.qmui_offTintColor; + } +} + + + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UITabBar+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UITabBar+QMUI.h index 72ba0472..6615e4c7 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITabBar+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UITabBar+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITabBar+QMUI.h @@ -14,21 +14,20 @@ // #import +#import "QMUIBarProtocol.h" -@interface UITabBar (QMUI) +NS_ASSUME_NONNULL_BEGIN -/** - UITabBar 的背景 view,可能显示磨砂、背景图,顶部有一部分溢出到 UITabBar 外。 - - 在 iOS 10 及以后是私有的 _UIBarBackground 类。 - - 在 iOS 9 及以前是私有的 _UITabBarBackgroundView 类。 - */ -@property(nonatomic, strong, readonly) UIView *qmui_backgroundView; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 + +UIKIT_EXTERN API_AVAILABLE(ios(13.0), tvos(13.0)) @interface UITabBarAppearance (QMUI) /** - qmui_backgroundView 内的 subview,用于显示顶部分隔线 shadowImage,注意这个 view 是溢出到 qmui_backgroundView 外的。若 shadowImage 为 [UIImage new],则这个 view 的高度为 0。 + 同时设置 stackedLayoutAppearance、inlineLayoutAppearance、compactInlineLayoutAppearance 三个状态下的 itemAppearance */ -@property(nonatomic, strong, readonly) UIImageView *qmui_shadowImageView; - +- (void)qmui_applyItemAppearanceWithBlock:(void (^)(UITabBarItemAppearance *itemAppearance))block; @end + +#endif + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UITabBar+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UITabBar+QMUI.m index a158b6f2..f45c428c 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITabBar+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UITabBar+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITabBar+QMUI.m @@ -14,11 +14,17 @@ // #import "UITabBar+QMUI.h" +#import "UITabBar+QMUIBarProtocol.h" #import "QMUICore.h" #import "UITabBarItem+QMUI.h" #import "UIBarItem+QMUI.h" +#import "UIImage+QMUI.h" +#import "UIView+QMUI.h" +#import "UINavigationController+QMUI.h" +#import "UIApplication+QMUI.h" NSInteger const kLastTouchedTabBarItemIndexNone = -1; +NSString *const kShouldCheckTabBarHiddenKey = @"kShouldCheckTabBarHiddenKey"; @interface UITabBar () @@ -33,129 +39,212 @@ @implementation UITabBar (QMUI) QMUISynthesizeNSIntegerProperty(lastTouchedTabBarItemViewIndex, setLastTouchedTabBarItemViewIndex) QMUISynthesizeNSIntegerProperty(tabBarItemViewTouchCount, setTabBarItemViewTouchCount) -- (UIView *)qmui_backgroundView { - return [self valueForKey:@"_backgroundView"]; -} - -- (UIImageView *)qmui_shadowImageView { - if (@available(iOS 10, *)) { - // iOS 10 及以后,在 UITabBar 初始化之后就能获取到 backgroundView 和 shadowView 了 - return [self.qmui_backgroundView valueForKey:@"_shadowView"]; - } - // iOS 9 及以前,shadowView 要在 UITabBar 第一次 layoutSubviews 之后才会被创建,直至 UITabBarController viewWillAppear: 时仍未能获取到 shadowView,所以为了省去调用时机的考虑,这里获取不到的时候会主动触发一次 tabBar 的布局 - UIImageView *shadowView = [self valueForKey:@"_shadowView"]; - if (!shadowView) { - [self setNeedsLayout]; - [self layoutIfNeeded]; - shadowView = [self valueForKey:@"_shadowView"]; - } - return shadowView; -} - + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ExtendImplementationOfVoidMethodWithTwoArguments([UITabBar class], @selector(setItems:animated:), NSArray *, BOOL, ^(UITabBar *selfObject, NSArray *items, BOOL animated) { - for (UITabBarItem *item in items) { - UIControl *itemView = (UIControl *)item.qmui_view; - [itemView addTarget:selfObject action:@selector(handleTabBarItemViewEvent:) forControlEvents:UIControlEventTouchUpInside]; - } + // -[UITabBar addSubview:] + OverrideImplementation([UITabBar class], @selector(addSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITabBar *selfObject, UIView *firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, UIView *); + originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if ([NSStringFromClass(firstArgv.class) isEqualToString:@"UITabBarButton"]) { + UIControl *button = (UIControl *)firstArgv; + [button addTarget:selfObject action:@selector(qmuitb_handleTabBarItemViewEvent:) forControlEvents:UIControlEventTouchUpInside]; + } + }; }); + // -[UITabBar setSelectedItem:] OverrideImplementation([UITabBar class], @selector(setSelectedItem:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UITabBar *selfObject, UITabBarItem *selectedItem) { + NSInteger olderSelectedIndex = selfObject.selectedItem ? [selfObject.items indexOfObject:selfObject.selectedItem] : -1; + // call super - void (^callSuperBlock)(UITabBarItem *) = ^void(UITabBarItem *aSelectedItem) { - void (*originSelectorIMP)(id, SEL, UITabBarItem *); - originSelectorIMP = (void (*)(id, SEL, UITabBarItem *))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, aSelectedItem); - }; + void (*originSelectorIMP)(id, SEL, UITabBarItem *); + originSelectorIMP = (void (*)(id, SEL, UITabBarItem *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, selectedItem); - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - NSInteger olderSelectedIndex = selfObject.selectedItem ? [selfObject.items indexOfObject:selfObject.selectedItem] : -1; - callSuperBlock(selectedItem); - NSInteger newerSelectedIndex = [selfObject.items indexOfObject:selectedItem]; - // 只有双击当前正在显示的界面的 tabBarItem,才能正常触发双击事件 - selfObject.canItemRespondDoubleTouch = olderSelectedIndex == newerSelectedIndex; - } else { - callSuperBlock(selectedItem); - } + NSInteger newerSelectedIndex = [selfObject.items indexOfObject:selectedItem]; + // 只有双击当前正在显示的界面的 tabBarItem,才能正常触发双击事件 + selfObject.canItemRespondDoubleTouch = olderSelectedIndex == newerSelectedIndex; }; }); - OverrideImplementation([UITabBar class], @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UITabBar *selfObject, CGRect frame) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (IOS_VERSION < 11.2 && IS_58INCH_SCREEN && ShouldFixTabBarTransitionBugInIPhoneX) { - if (CGRectGetHeight(frame) == TabBarHeight && CGRectGetMaxY(frame) < CGRectGetHeight(selfObject.superview.bounds)) { - // iOS 11 在界面 push 的过程中 tabBar 会瞬间往上跳,所以做这个修复。这个 bug 在 iOS 11.2 里已被系统修复。 - // https://github.com/Tencent/QMUI_iOS/issues/217 - frame = CGRectSetY(frame, CGRectGetHeight(selfObject.superview.bounds) - CGRectGetHeight(frame)); - } - } - - // 修复这个 bug:https://github.com/Tencent/QMUI_iOS/issues/309 - if (@available(iOS 11, *)) { - if (IS_NOTCHED_SCREEN && ((CGRectGetHeight(frame) == 49 || CGRectGetHeight(frame) == 32))) {// 只关注全面屏设备下的这两种非正常的 tabBar 高度即可 - CGFloat bottomSafeAreaInsets = selfObject.safeAreaInsets.bottom > 0 ? selfObject.safeAreaInsets.bottom : selfObject.superview.safeAreaInsets.bottom;// 注意,如果只是拿 selfObject.safeAreaInsets 判断,会肉眼看到高度的跳变,因此引入 superview 的值(虽然理论上 tabBar 不一定都会布局到 UITabBarController.view 的底部) - if (bottomSafeAreaInsets == CGRectGetHeight(selfObject.frame)) { - return;// 由于这个系统 bug https://github.com/Tencent/QMUI_iOS/issues/446,这里先暂时屏蔽本次 frame 变化 - } - frame.size.height += bottomSafeAreaInsets; - frame.origin.y -= bottomSafeAreaInsets; - } - } - } + // iOS 13 下如果以 UITabBarAppearance 的方式将 UITabBarItem 的 font 大小设置为超过默认的 10,则会出现布局错误,文字被截断,所以这里做了个兼容,iOS 14.0 测试过已不存在该问题 + // https://github.com/Tencent/QMUI_iOS/issues/740 + // + // iOS 14 修改 UITabBarAppearance.inlineLayoutAppearance.normal.titleTextAttributes[NSForegroundColor] 会导致 UITabBarItem 文字无法完整展示 + // https://github.com/Tencent/QMUI_iOS/issues/1110 + // + // [UIKit Bug] 使用 UITabBarAppearance 将 UITabBarItem 选中时的字体设置为 bold 则无法完整显示 title + // https://github.com/Tencent/QMUI_iOS/issues/1286 + OverrideImplementation(NSClassFromString(@"UITabBarButtonLabel"), @selector(setAttributedText:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UILabel *selfObject, NSAttributedString *firstArgv) { // call super - void (*originSelectorIMP)(id, SEL, CGRect); - originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, frame); + void (*originSelectorIMP)(id, SEL, NSAttributedString *); + originSelectorIMP = (void (*)(id, SEL, NSAttributedString *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if (@available(iOS 14.0, *)) { + // iOS 14 只有在 bold 时才有问题,所以把额外的 sizeToFit 做一些判断,尽量减少调用次数 + UIFont *font = selfObject.font; + BOOL isBold = [font.fontName containsString:@"bold"]; + if (isBold) { + [selfObject sizeToFit]; + } + } else { + // iOS 13 加粗时有 #1286 描述的问题,不加粗时有 #740 描述的问题,所以干脆只要是 iOS 13 都加粗算了 + [selfObject sizeToFit]; + } }; }); - // 以下代码修复两个仅存在于 12.1.0 版本的系统 bug,实测 12.1.1 苹果已经修复 - if (@available(iOS 12.1, *)) { - - OverrideImplementation(NSClassFromString(@"UITabBarButton"), @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIView *selfObject, CGRect firstArgv) { - - if ([selfObject isKindOfClass:originClass]) { - // Fixed: UITabBar layout is broken on iOS 12.1 - // https://github.com/Tencent/QMUI_iOS/issues/410 + // iOS 14.0 如果 pop 到一个 hidesBottomBarWhenPushed = NO 的 vc,tabBar 无法正确显示出来 + // 根据测试,iOS 14.2 开始,系统已修复该问题 + // https://github.com/Tencent/QMUI_iOS/issues/1100 + if (@available(iOS 14.0, *)) { + if (@available(iOS 14.2, *)) { + } else { + OverrideImplementation([UINavigationController class], @selector(qmui_didInitialize), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationController *selfObject) { - if (IOS_VERSION_NUMBER < 120101 || (QMUICMIActivated && ShouldFixTabBarButtonBugForAll)) { - if (!CGRectIsEmpty(selfObject.frame) && CGRectIsEmpty(firstArgv)) { - return; - } - } + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); - if (IOS_VERSION_NUMBER < 120101) { - // Fixed: iOS 12.1 UITabBarItem positioning issue during swipe back gesture (when UINavigationBar is hidden) - // https://github.com/Tencent/QMUI_iOS/issues/422 - if (IS_NOTCHED_SCREEN) { - if ((CGRectGetHeight(selfObject.frame) == 48 && CGRectGetHeight(firstArgv) == 33) || (CGRectGetHeight(selfObject.frame) == 31 && CGRectGetHeight(firstArgv) == 20)) { - return; + [selfObject qmui_addNavigationActionDidChangeBlock:^(QMUINavigationAction action, BOOL animated, __kindof UINavigationController * _Nullable weakNavigationController, __kindof UIViewController * _Nullable appearingViewController, NSArray<__kindof UIViewController *> * _Nullable disappearingViewControllers) { + switch (action) { + case QMUINavigationActionWillPop: + case QMUINavigationActionWillSet: { + // 系统的逻辑就是,在 push N 个 vc 的过程中,只要其中出现任意一个 vc.hidesBottomBarWhenPushed = YES,则 tabBar 不会再出现(不管后续有没有 vc.hidesBottomBarWhenPushed = NO),所以在 pop 回去的时候也要遵循这个规则 + if (animated && weakNavigationController.tabBarController && !appearingViewController.hidesBottomBarWhenPushed) { + BOOL systemShouldHideTabBar = NO; + + // setViewControllers 可能出现当前 vc 不存在已有 viewControllers 数组内的情况,要保护 + // https://github.com/Tencent/QMUI_iOS/issues/1177 + NSUInteger index = [weakNavigationController.viewControllers indexOfObject:appearingViewController]; + + if (index != NSNotFound) { + NSArray *viewControllers = [weakNavigationController.viewControllers subarrayWithRange:NSMakeRange(0, index + 1)]; + for (UIViewController *vc in viewControllers) { + if (vc.hidesBottomBarWhenPushed) { + systemShouldHideTabBar = YES; + } + } + if (!systemShouldHideTabBar) { + [weakNavigationController qmui_bindBOOL:YES forKey:kShouldCheckTabBarHiddenKey]; + } + } + } } + break; + case QMUINavigationActionDidPop: + case QMUINavigationActionDidSet: { + [weakNavigationController qmui_bindBOOL:NO forKey:kShouldCheckTabBarHiddenKey]; + } + break; + + default: + break; } + }]; + }; + }); + + OverrideImplementation([UINavigationController class], NSSelectorFromString(@"_shouldBottomBarBeHidden"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^BOOL(UINavigationController *selfObject) { + // call super + BOOL (*originSelectorIMP)(id, SEL); + originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider(); + BOOL result = originSelectorIMP(selfObject, originCMD); + + if ([selfObject qmui_getBoundBOOLForKey:kShouldCheckTabBarHiddenKey]) { + result = NO; } - } - - // call super - void (*originSelectorIMP)(id, SEL, CGRect); - originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, firstArgv); - }; - }); + return result; + }; + }); + } } + + + // 以下是将 iOS 12 修改 UITabBar 样式的接口转换成用 iOS 13 的新接口去设置(因为新旧方法是互斥的,所以统一在新系统都用新方法) + // 但这样有个风险,因为 QMUIConfiguration 配置表里都是用 appearance 的方式去设置 standardAppearance,所以如果在 UITabBar 实例被添加到 window 之前修改过旧版任意一个样式接口,就会导致一个新的 UITabBarAppearance 对象被设置给 standardAppearance 属性,这样系统就会认为你这个 UITabBar 实例自定义了 standardAppearance,那么当它被 moveToWindow 时就不会自动应用 appearance 的值了,因此需要保证在添加到 window 前不要自行修改属性 + void (^syncAppearance)(UITabBar *, void(^barActionBlock)(UITabBarAppearance *appearance), void (^itemActionBlock)(UITabBarItemAppearance *itemAppearance)) = ^void(UITabBar *tabBar, void(^barActionBlock)(UITabBarAppearance *appearance), void (^itemActionBlock)(UITabBarItemAppearance *itemAppearance)) { + if (!barActionBlock && !itemActionBlock) return; + + UITabBarAppearance *appearance = tabBar.standardAppearance; + if (barActionBlock) { + barActionBlock(appearance); + } + if (itemActionBlock) { + [appearance qmui_applyItemAppearanceWithBlock:itemActionBlock]; + } + tabBar.standardAppearance = appearance; +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + if (QMUICMIActivated && TabBarUsesStandardAppearanceOnly) { + tabBar.scrollEdgeAppearance = appearance; + } + } +#endif + }; + + ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setTintColor:), UIColor *, ^(UITabBar *selfObject, UIColor *tintColor) { + syncAppearance(selfObject, nil, ^void(UITabBarItemAppearance *itemAppearance) { + itemAppearance.selected.iconColor = tintColor; + + NSMutableDictionary *textAttributes = itemAppearance.selected.titleTextAttributes.mutableCopy; + textAttributes[NSForegroundColorAttributeName] = tintColor; + itemAppearance.selected.titleTextAttributes = textAttributes.copy; + }); + }); + + ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setBarTintColor:), UIColor *, ^(UITabBar *selfObject, UIColor *barTintColor) { + syncAppearance(selfObject, ^void(UITabBarAppearance *appearance) { + appearance.backgroundColor = barTintColor; + }, nil); + }); + + ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setUnselectedItemTintColor:), UIColor *, ^(UITabBar *selfObject, UIColor *tintColor) { + syncAppearance(selfObject, nil, ^void(UITabBarItemAppearance *itemAppearance) { + itemAppearance.normal.iconColor = tintColor; + + NSMutableDictionary *textAttributes = itemAppearance.normal.titleTextAttributes.mutableCopy; + textAttributes[NSForegroundColorAttributeName] = tintColor; + itemAppearance.normal.titleTextAttributes = textAttributes.copy; + }); + }); + + ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setBackgroundImage:), UIImage *, ^(UITabBar *selfObject, UIImage *image) { + syncAppearance(selfObject, ^void(UITabBarAppearance *appearance) { + appearance.backgroundImage = image; + }, nil); + }); + + ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setShadowImage:), UIImage *, ^(UITabBar *selfObject, UIImage *shadowImage) { + syncAppearance(selfObject, ^void(UITabBarAppearance *appearance) { + appearance.shadowImage = shadowImage; + }, nil); + }); + + ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setBarStyle:), UIBarStyle, ^(UITabBar *selfObject, UIBarStyle barStyle) { + syncAppearance(selfObject, ^void(UITabBarAppearance *appearance) { + appearance.backgroundEffect = [UIBlurEffect effectWithStyle:barStyle == UIBarStyleDefault ? UIBlurEffectStyleSystemChromeMaterialLight : UIBlurEffectStyleSystemChromeMaterialDark]; + }, nil); + }); }); } -- (void)handleTabBarItemViewEvent:(UIControl *)itemView { +- (void)qmuitb_handleTabBarItemViewEvent:(UIControl *)itemView { if (!self.canItemRespondDoubleTouch) { return; @@ -167,7 +256,7 @@ - (void)handleTabBarItemViewEvent:(UIControl *)itemView { // 如果一定时间后仍未触发双击,则废弃当前的点击状态 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self revertTabBarItemTouch]; + [self qmuitb_revertTabBarItemTouch]; }); NSInteger selectedIndex = [self.items indexOfObject:self.selectedItem]; @@ -177,7 +266,7 @@ - (void)handleTabBarItemViewEvent:(UIControl *)itemView { self.lastTouchedTabBarItemViewIndex = selectedIndex; } else if (self.lastTouchedTabBarItemViewIndex != selectedIndex) { // 后续的点击如果与第一次点击的 index 不一致,则认为是重新开始一次新的点击 - [self revertTabBarItemTouch]; + [self qmuitb_revertTabBarItemTouch]; self.lastTouchedTabBarItemViewIndex = selectedIndex; return; } @@ -189,13 +278,23 @@ - (void)handleTabBarItemViewEvent:(UIControl *)itemView { if (item.qmui_doubleTapBlock) { item.qmui_doubleTapBlock(item, selectedIndex); } - [self revertTabBarItemTouch]; + [self qmuitb_revertTabBarItemTouch]; } } -- (void)revertTabBarItemTouch { +- (void)qmuitb_revertTabBarItemTouch { self.lastTouchedTabBarItemViewIndex = kLastTouchedTabBarItemIndexNone; self.tabBarItemViewTouchCount = 0; } @end + +@implementation UITabBarAppearance (QMUI) + +- (void)qmui_applyItemAppearanceWithBlock:(void (^)(UITabBarItemAppearance * _Nonnull))block { + block(self.stackedLayoutAppearance); + block(self.inlineLayoutAppearance); + block(self.compactInlineLayoutAppearance); +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.h index c32aff41..47fc81c0 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITabBarItem+QMUI.h @@ -15,21 +15,23 @@ #import +NS_ASSUME_NONNULL_BEGIN + @interface UITabBarItem (QMUI) /** * 双击 tabBarItem 时的回调,默认为 nil。 - * @arg tabBarItem 被双击的 UITabBarItem - * @arg index 被双击的 UITabBarItem 的序号 + * @param tabBarItem 被双击的 UITabBarItem,若需要拿到当前的 view 则通过 qmui_view 获取。 + * @param index 被双击的 UITabBarItem 的序号 */ -@property(nonatomic, copy) void (^qmui_doubleTapBlock)(UITabBarItem *tabBarItem, NSInteger index); +@property(nonatomic, copy, nullable) void (^qmui_doubleTapBlock)(UITabBarItem *tabBarItem, NSInteger index); /** * 获取一个UITabBarItem内显示图标的UIImageView,如果找不到则返回nil - * @warning 需要对nil的返回值做保护 */ -- (UIImageView *)qmui_imageView; - -+ (UIImageView *)qmui_imageViewInTabBarButton:(UIView *)tabBarButton; +- (nullable UIImageView *)qmui_imageView; ++ (nullable UIImageView *)qmui_imageViewInTabBarButton:(nullable UIView *)tabBarButton; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.m index ef4d4116..c3cd55dd 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITabBarItem+QMUI.m @@ -30,22 +30,7 @@ + (UIImageView *)qmui_imageViewInTabBarButton:(UIView *)tabBarButton { if (!tabBarButton) { return nil; } - - for (UIView *subview in tabBarButton.subviews) { - // iOS10及以后,imageView都是用UITabBarSwappableImageView实现的,所以遇到这个class就直接拿 - if ([NSStringFromClass([subview class]) isEqualToString:@"UITabBarSwappableImageView"]) { - return (UIImageView *)subview; - } - - if (IOS_VERSION < 10) { - // iOS10以前,选中的item的高亮是用UITabBarSelectionIndicatorView实现的,所以要屏蔽掉 - if ([subview isKindOfClass:[UIImageView class]] && ![NSStringFromClass([subview class]) isEqualToString:@"UITabBarSelectionIndicatorView"]) { - return (UIImageView *)subview; - } - } - - } - return nil; + return [tabBarButton qmui_valueForKey:@"_imageView"]; } @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.h index 7d175e4d..7c83ce4f 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITableView+QMUI.h @@ -18,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN +#define PreferredValueForTableViewStyle(_style, _plain, _grouped, _insetGrouped) (_style == UITableViewStyleGrouped ? _grouped : (_style == UITableViewStyleInsetGrouped ? _insetGrouped : _plain)) + /// cell 在当前 section 里的位置,注意判断时要用 (var & xxx) == xxx 的方式 typedef NS_OPTIONS(NSInteger, QMUITableViewCellPosition) { QMUITableViewCellPositionNone = 0, // 默认 @@ -48,11 +50,6 @@ typedef NS_OPTIONS(NSInteger, QMUITableViewCellPosition) { /// 将当前tableView按照QMUI统一定义的宏来渲染外观 - (void)qmui_styledAsQMUITableView; -/** - 列表默认的 contentInset,会自动将 contentInset 和 scrollIndicatorInsets 都设置为这个值并且调用一次 qmui_scrollToTopUponContentInsetTopChange 设置默认的 contentOffset,一般用于 UIScrollViewContentInsetAdjustmentNever 的列表。 - */ -@property(nonatomic, assign) UIEdgeInsets qmui_initialContentInset; - /** * 获取某个 view 在 tableView 里的 indexPath * @@ -70,10 +67,10 @@ typedef NS_OPTIONS(NSInteger, QMUITableViewCellPosition) { */ - (NSInteger)qmui_indexForSectionHeaderAtView:(nullable UIView *)view; -/// 获取可视范围内的所有 sectionHeader 的 index +/// 获取可视范围内的所有 sectionHeader 的 index,注意 contentInset 所在的区域被视为“不可视”。 @property(nonatomic, readonly, nullable) NSArray *qmui_indexForVisibleSectionHeaders; -/// 获取正处于 pinned(悬停在顶部)状态的 sectionHeader 的序号 +/// 获取正处于 pinned(悬停在顶部)状态的 sectionHeader 的序号,注意如果某个 section 的 numberOfRows 为 0,则这个 section 天然无法被 pinned。 @property(nonatomic, readonly) NSInteger qmui_indexOfPinnedSectionHeader; /** @@ -104,6 +101,9 @@ typedef NS_OPTIONS(NSInteger, QMUITableViewCellPosition) { */ - (void)qmui_scrollToRowFittingOffsetY:(CGFloat)offsetY atIndexPath:(nonnull NSIndexPath *)indexPath animated:(BOOL)animated; +/// 获取当前 UITableView 用于呈现内容的区域的宽度,例如在全面屏下会减去 safeAreaInsets.left/right,在 InsetGrouped 样式下会减去水平的缩进 +@property(nonatomic, assign, readonly) CGFloat qmui_validContentWidth; + /** * 当tableHeaderView为UISearchBar时,tableView为了实现searchbar滚到顶部自动吸附的效果,会强制让self.contentSize.height至少为frame.size.height那么高(这样才能滚动,否则不满一屏就无法滚动了),所以此时如果通过self.contentSize获取tableView的内容大小是不准确的,此时可以使用`qmui_realContentSize`替代。 * @@ -122,7 +122,26 @@ typedef NS_OPTIONS(NSInteger, QMUITableViewCellPosition) { @param updates insert/delete/reload/move calls @param completion completion callback */ -- (void)qmui_performBatchUpdates:(void (NS_NOESCAPE ^ _Nullable)(void))updates completion:(void (^ _Nullable)(BOOL finished))completion; +- (void)qmui_performBatchUpdates:(void (NS_NOESCAPE ^ _Nullable)(void))updates completion:(void (^ _Nullable)(BOOL finished))completion DEPRECATED_MSG_ATTRIBUTE("请使用系统的 -[UITableView performBatchUpdates:completion:],QMUI 4.4.0 已不再支持 iOS 10,没必要提供该兼容性之的接口了,后续会删除。"); + +@end + +/** + 系统在 iOS 13 新增了 UITableViewStyleInsetGrouped 类型用于展示往内缩进、cell 带圆角的列表,而这个 Category 让 iOS 12 及以下的系统也能支持这种样式,iOS 13 也可以通过这个 Category 修改左右的缩进值和 cell 的圆角。 + 使用方式: + 对于 UITableView,通过 -[UITableView initWithStyle:UITableViewStyleInsetGrouped] 初始化 tableView。 + 对于 UITableViewController,通过 -[UITableViewController initWithStyle:UITableViewStyleInsetGrouped] 初始化 tableViewController。 + 可通过 @c qmui_insetGroupedCornerRadius @c qmui_insetGroupedHorizontalInset 统一修改圆角值和左右缩进,如果要为不同 indexPath 指定不同圆角值,可在 -[UITableViewDelegate tableView:willDisplayCell:forRowAtIndexPath:] 内修改 cell.layer.cornerRadius 的值。 + + @note 对于 sectionHeader/footer,建议使用 QMUITableViewHeaderFooterView,或者继承系统的 UITableViewHeaderFooterView 并重写它的 sizeThatFits:、layoutSubviews 去计算高度和布局,sizeThatFits: 的参数 size.width 即为减去左右缩进后的宽度。如果直接用系统的 UITableViewHeaderFooterView,iOS 10 及以下多行文本时布局会错误,暂时无法解决,但如果业务项目本身不需要支持 iOS 10 及以下系统,那可忽略这个限制。 + */ +@interface UITableView (QMUI_InsetGrouped) + +/// 当使用 UITableViewStyleInsetGrouped 时可通过这个属性修改 cell 的圆角值,默认值为 10,也即 iOS 13 系统默认表现。如果要为不同 indexPath 指定不同圆角值,可在 -[UITableViewDelegate tableView:willDisplayCell:forRowAtIndexPath:] 内修改 cell.layer.cornerRadius 的值。 +@property(nonatomic, assign) CGFloat qmui_insetGroupedCornerRadius UI_APPEARANCE_SELECTOR; + +/// 当使用 UITableViewStyleInsetGrouped 时可通过这个属性修改列表的左右缩进值,默认值为 20,也即 iOS 13 系统默认表现。 +@property(nonatomic, assign) CGFloat qmui_insetGroupedHorizontalInset UI_APPEARANCE_SELECTOR; @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.m index 42926c50..7850986a 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITableView+QMUI.m @@ -13,43 +13,49 @@ // Created by QMUI Team on 15/7/20. // +#import "UIView+QMUI.h" #import "UITableView+QMUI.h" +#import "UITableViewCell+QMUI.h" #import "QMUICore.h" #import "UIScrollView+QMUI.h" #import "QMUILog.h" #import "NSObject+QMUI.h" +#import "CALayer+QMUI.h" const NSUInteger kFloatValuePrecision = 4;// 统一一个小数点运算精度 +@interface UITableView () +@property(nonatomic, assign, readonly) CGRect qmui_indexFrame; +@end + @implementation UITableView (QMUI) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ExtendImplementationOfNonVoidMethodWithTwoArguments([UITableView class], @selector(initWithFrame:style:), CGRect, UITableViewStyle, UITableView *, ^UITableView *(UITableView *selfObject, CGRect frame, UITableViewStyle style, UITableView *originReturnValue) { - // iOS 11 之后 estimatedRowHeight 如果值为 UITableViewAutomaticDimension,estimate 效果也会生效(iOS 11 以前要 > 0 才会生效)。 - // 而当使用 estimate 效果时,会导致 contentSize 之类的计算不准确,所以这里给一个途径让项目可以方便地控制 QMUITableView(及其子类) 和 UITableView(不包含子类,例如 UIPickerTableView)的 estimatedRowHeight 效果的开关 https://github.com/Tencent/QMUI_iOS/issues/313 - if ([selfObject isKindOfClass:NSClassFromString(@"QMUITableView")] || [NSStringFromClass(selfObject.class) isEqualToString:@"UITableView"]) { - if (TableViewEstimatedHeightEnabled) { - selfObject.estimatedRowHeight = TableViewCellNormalHeight; - selfObject.estimatedSectionHeaderHeight = TableViewCellNormalHeight; - selfObject.estimatedSectionFooterHeight = TableViewCellNormalHeight; - } else { - selfObject.estimatedRowHeight = 0; - selfObject.estimatedSectionHeaderHeight = 0; - selfObject.estimatedSectionFooterHeight = 0; + OverrideImplementation([UITableView class], @selector(initWithFrame:style:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UITableView *(UITableView *selfObject, CGRect firstArgv, UITableViewStyle secondArgv) { + + // call super + UITableView *(*originSelectorIMP)(id, SEL, CGRect, UITableViewStyle); + originSelectorIMP = (UITableView * (*)(id, SEL, CGRect, UITableViewStyle))originalIMPProvider(); + UITableView *result = originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); + + // iOS 11 之后 estimatedRowHeight 如果值为 UITableViewAutomaticDimension,estimate 效果也会生效(iOS 11 以前要 > 0 才会生效)。 + // 而当使用 estimate 效果时,会导致 contentSize 之类的计算不准确,所以这里给一个途径让项目可以方便地控制 UITableView(不包含子类,例如 UIPickerTableView)的 estimatedRowHeight 效果的开关,至于 QMUITableView 会在自己内部 init 时调用 + // https://github.com/Tencent/QMUI_iOS/issues/313 + if (QMUICMIActivated && [NSStringFromClass(selfObject.class) isEqualToString:@"UITableView"]) { + [selfObject _qmui_configEstimatedRowHeight]; } - } - return originReturnValue; + + return result; + }; }); OverrideImplementation([UITableView class], @selector(sizeThatFits:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^CGSize(UITableView *selfObject, CGSize size) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - [selfObject alertEstimatedHeightUsageIfDetected]; - } + [selfObject alertEstimatedHeightUsageIfDetected]; // call super CGSize (*originSelectorIMP)(id, SEL, CGSize); @@ -63,33 +69,121 @@ + (void)load { OverrideImplementation([UITableView class], @selector(scrollToRowAtIndexPath:atScrollPosition:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UITableView *selfObject, NSIndexPath *indexPath, UITableViewScrollPosition scrollPosition, BOOL animated) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (!indexPath) { - return; - } + if (!indexPath) { + return; + } + + BOOL isIndexPathLegal = YES; + NSInteger numberOfSections = [selfObject numberOfSections]; + if (indexPath.section < 0 || indexPath.section >= numberOfSections) { + isIndexPathLegal = NO; + } else if (indexPath.row != NSNotFound) { + NSInteger rows = [selfObject numberOfRowsInSection:indexPath.section]; + isIndexPathLegal = indexPath.row >= 0 && indexPath.row < rows; + } + if (!isIndexPathLegal) { + QMUIAssert(NO, @"UITableView (QMUI)", @"%@ - target indexPath : %@ ,不合法的indexPath。\n%@", selfObject, indexPath, [NSThread callStackSymbols]); + return; + } + + // call super + void (*originSelectorIMP)(id, SEL, NSIndexPath *, UITableViewScrollPosition, BOOL); + originSelectorIMP = (void (*)(id, SEL, NSIndexPath *, UITableViewScrollPosition, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, indexPath, scrollPosition, animated); + }; + }); + + // [UIKit Bug] 将 UISearchBar 作为 tableHeaderView 使用的 UITableView,如果同时设置了 estimatedRowHeight,则 contentSize 会错乱,导致滚动异常 + // https://github.com/Tencent/QMUI_iOS/issues/1161 + if (@available(iOS 15.0, *)) { + } else { + /* - (void)_coalesceContentSizeUpdateWithDelta:(double)arg1; */ + OverrideImplementation([UITableView class], NSSelectorFromString(@"_coalesceContentSizeUpdateWithDelta:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITableView *selfObject, CGFloat firstArgv) { - BOOL isIndexPathLegal = YES; - NSInteger numberOfSections = [selfObject numberOfSections]; - if (indexPath.section >= numberOfSections) { - isIndexPathLegal = NO; - } else if (indexPath.row != NSNotFound) { - NSInteger rows = [selfObject numberOfRowsInSection:indexPath.section]; - isIndexPathLegal = indexPath.row < rows; + // call super + void (*originSelectorIMP)(id, SEL, CGFloat); + originSelectorIMP = (void (*)(id, SEL, CGFloat))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + BOOL estimatesRowHeight = NO; + [selfObject qmui_performSelector:NSSelectorFromString(@"_estimatesRowHeights") withPrimitiveReturnValue:&estimatesRowHeight]; + if (estimatesRowHeight && [selfObject.tableHeaderView isKindOfClass:UISearchBar.class]) { + BeginIgnorePerformSelectorLeaksWarning + [selfObject performSelector:NSSelectorFromString(@"_updateContentSize")]; + EndIgnorePerformSelectorLeaksWarning } - if (!isIndexPathLegal) { - QMUILogWarn(@"UITableView (QMUI)", @"%@ - target indexPath : %@ ,不合法的indexPath。\n%@", selfObject, indexPath, [NSThread callStackSymbols]); - if (QMUICMIActivated && !ShouldPrintQMUIWarnLogToConsole) { - NSAssert(NO, @"出现不合法的indexPath"); + }; + }); + } + + OverrideImplementation([UITableView class], @selector(reloadData), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITableView *selfObject) { + + // [UIKit Bug] iOS 11 及以上,关闭 estimated height 的 tableView 可能出现数据错乱引发 crash + // https://github.com/Tencent/QMUI_iOS/issues/1243 + if (![selfObject qmui_getBoundBOOLForKey:@"kHasCalledReloadDataOnce"] && [selfObject.dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) { + NSInteger a = [selfObject.dataSource numberOfSectionsInTableView:selfObject]; + NSInteger b = [selfObject numberOfSections]; + if (a == 0 && b == 1) { + // - [UITableView noteNumberOfRowsChanged] + SEL selector = NSSelectorFromString([NSString qmui_stringByConcat:@"note", @"NumberOfRows", @"Changed", nil]); + if ([selfObject respondsToSelector:selector]) { + BeginIgnorePerformSelectorLeaksWarning + [selfObject performSelector:selector]; + EndIgnorePerformSelectorLeaksWarning } - return; } } // call super - void (*originSelectorIMP)(id, SEL, NSIndexPath *, UITableViewScrollPosition, BOOL); - originSelectorIMP = (void (*)(id, SEL, NSIndexPath *, UITableViewScrollPosition, BOOL))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, indexPath, scrollPosition, animated); + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + // 记录是否执行过一次 reloadData,目的是只对第一次 reloadData 做检查 + [selfObject qmui_bindBOOL:YES forKey:@"kHasCalledReloadDataOnce"]; + + // [UIKit Bug] 将 UISearchBar 作为 tableHeaderView 使用的 UITableView,在 tableView 尚未添加到 window 上就同时进行了 setTableHeaderView:、reloadData 的操作,会导致滚动异常 + // https://github.com/Tencent/QMUI_iOS/issues/1215 + // 简单用“存在 superview 却不存在 window”的方式来区分该 UITableView 是 UIViewController 里的还是 UITableViewController 里的 + if (!selfObject.window && selfObject.superview && [selfObject.tableHeaderView isKindOfClass:UISearchBar.class]) { + [selfObject qmui_bindBOOL:YES forKey:@"kShouldFixContentSizeBugKey"]; + } + }; + }); + OverrideImplementation([UITableView class], @selector(didMoveToWindow), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITableView *selfObject) { + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + if ([selfObject qmui_getBoundBOOLForKey:@"kShouldFixContentSizeBugKey"]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [selfObject reloadData]; + }); + [selfObject qmui_bindBOOL:NO forKey:@"kShouldFixContentSizeBugKey"]; + } + }; + }); + + // [UIKit Bug] UISearchBar 作为 tableHeaderView 使用时,切换 tableView 的 sectionIndex 的显隐,searchBar 的布局可能无法刷新 + // https://github.com/Tencent/QMUI_iOS/issues/1213 + OverrideImplementation([UITableView class], NSSelectorFromString(@"_removeIndex"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITableView *selfObject) { + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + UISearchBar *searchBar = (UISearchBar *)selfObject.tableHeaderView; + if ([searchBar isKindOfClass:UISearchBar.class]) { + // UISearchBar 内部通过这个私有方法来根据 UITableView 的状态刷新自身的 inset,这里手动调用一次 + [searchBar qmui_performSelector:NSSelectorFromString(@"_updateInsetsForTableView:") withArguments:&selfObject, nil]; + } }; }); }); @@ -110,10 +204,7 @@ - (void)qmui_scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(U isIndexPathLegal = indexPath.row < rows; } if (!isIndexPathLegal) { - QMUILogWarn(@"UITableView (QMUI)", @"%@ - target indexPath : %@ ,不合法的indexPath。\n%@", self, indexPath, [NSThread callStackSymbols]); - if (QMUICMIActivated && !ShouldPrintQMUIWarnLogToConsole) { - NSAssert(NO, @"出现不合法的indexPath"); - } + QMUIAssert(NO, @"UITableView (QMUI)", @"%@ - target indexPath : %@ ,不合法的indexPath。\n%@", self, indexPath, [NSThread callStackSymbols]); } else { [self qmui_scrollToRowAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated]; } @@ -121,36 +212,54 @@ - (void)qmui_scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(U - (void)qmui_styledAsQMUITableView { - self.rowHeight = TableViewCellNormalHeight; + if (!QMUICMIActivated) return; + + [self _qmui_configEstimatedRowHeight]; + + self.backgroundColor = PreferredValueForTableViewStyle(self.style, TableViewBackgroundColor, TableViewGroupedBackgroundColor, TableViewInsetGroupedBackgroundColor); + self.separatorColor = PreferredValueForTableViewStyle(self.style, TableViewSeparatorColor, TableViewGroupedSeparatorColor, TableViewInsetGroupedSeparatorColor); + - UIColor *backgroundColor = nil; + // 去掉空白的cell if (self.style == UITableViewStylePlain) { - backgroundColor = TableViewBackgroundColor; - self.tableFooterView = [[UIView alloc] init]; // 去掉空白的cell - } else { - backgroundColor = TableViewGroupedBackgroundColor; - } - if (backgroundColor) { - self.backgroundColor = backgroundColor; + self.tableFooterView = [[UIView alloc] init]; } - self.separatorColor = TableViewSeparatorColor; - self.backgroundView = [[UIView alloc] init]; // 设置一个空的 backgroundView,去掉系统自带的,以使 backgroundColor 生效 + + self.backgroundView = [[UIView alloc] init]; // 设置一个空的 backgroundView,去掉系统自带的,以使 backgroundColor 生效(系统在 tableHeaderView 为 UISearchBar 时会自动设置一层背景灰色,导致背景色看不到。只有使用了自定义 backgroundView 才能屏蔽系统这个行为) self.sectionIndexColor = TableSectionIndexColor; self.sectionIndexTrackingBackgroundColor = TableSectionIndexTrackingBackgroundColor; self.sectionIndexBackgroundColor = TableSectionIndexBackgroundColor; +#ifdef IOS15_SDK_ALLOWED + if (@available(iOS 15.0, *)) { + self.sectionHeaderTopPadding = PreferredValueForTableViewStyle(self.style, TableViewSectionHeaderTopPadding, TableViewGroupedSectionHeaderTopPadding, TableViewInsetGroupedSectionHeaderTopPadding); + } +#endif + + self.qmui_insetGroupedCornerRadius = TableViewInsetGroupedCornerRadius; + self.qmui_insetGroupedHorizontalInset = TableViewInsetGroupedHorizontalInset; } -static char kAssociatedObjectKey_initialContentInset; -- (void)setQmui_initialContentInset:(UIEdgeInsets)qmui_initialContentInset { - objc_setAssociatedObject(self, &kAssociatedObjectKey_initialContentInset, [NSValue valueWithUIEdgeInsets:qmui_initialContentInset], OBJC_ASSOCIATION_RETAIN_NONATOMIC); - self.contentInset = qmui_initialContentInset; - self.scrollIndicatorInsets = qmui_initialContentInset; - [self qmui_scrollToTopUponContentInsetTopChange]; -} - -- (UIEdgeInsets)qmui_initialContentInset { - return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_initialContentInset)) UIEdgeInsetsValue]; +- (void)_qmui_configEstimatedRowHeight { + if (TableViewEstimatedHeightEnabled) { + self.estimatedRowHeight = TableViewCellNormalHeight; + self.rowHeight = UITableViewAutomaticDimension; + + self.estimatedSectionHeaderHeight = UITableViewAutomaticDimension; + self.sectionHeaderHeight = UITableViewAutomaticDimension; + + self.estimatedSectionFooterHeight = UITableViewAutomaticDimension; + self.sectionFooterHeight = UITableViewAutomaticDimension; + } else { + self.estimatedRowHeight = 0; + self.rowHeight = TableViewCellNormalHeight; + + self.estimatedSectionHeaderHeight = 0; + self.sectionHeaderHeight = UITableViewAutomaticDimension; + + self.estimatedSectionFooterHeight = 0; + self.sectionFooterHeight = UITableViewAutomaticDimension; + } } - (NSIndexPath *)qmui_indexPathForRowAtView:(UIView *)view { @@ -176,31 +285,36 @@ - (NSInteger)qmui_indexForSectionHeaderAtView:(UIView *)view { CGPoint origin = [self convertPoint:view.frame.origin fromView:view.superview]; origin = CGPointToFixed(origin, kFloatValuePrecision);// 避免一些浮点数精度问题导致的计算错误 - NSUInteger numberOfSection = [self numberOfSections]; - // TODO: molice 针对 section 特别多的场景,优化一下这里的遍历查找 - for (NSInteger i = 0; i < numberOfSection; i++) { - CGRect rectForSection = [self rectForSection:i];// TODO: 这里的判断用整个 section 的 rect,可能需要加上“view 是否在 sectionHeader 上的判断” + NSInteger low = 0; + NSInteger high = [self numberOfSections]; + while (low <= high) { + NSInteger mid = low + ((high-low) >> 1); + CGRect rectForSection = [self rectForSection:mid]; rectForSection = CGRectToFixed(rectForSection, kFloatValuePrecision); if (CGRectContainsPoint(rectForSection, origin)) { - return i; + UITableViewHeaderFooterView *headerView = [self headerViewForSection:mid]; + if (headerView && [view isDescendantOfView:headerView]) { + return mid; + } else { + return -1; + } + } else if (rectForSection.origin.y < origin.y) { + low = mid + 1; + } else { + high = mid - 1; } } return -1; } - (NSArray *)qmui_indexForVisibleSectionHeaders { - NSArray *visibleCellIndexPaths = [self indexPathsForVisibleRows]; - NSMutableArray *visibleSections = [[NSMutableArray alloc] init]; - NSMutableArray *result = [[NSMutableArray alloc] init]; - for (NSInteger i = 0; i < visibleCellIndexPaths.count; i++) { - if (visibleSections.count == 0 || visibleCellIndexPaths[i].section != visibleSections.lastObject.integerValue) { - [visibleSections addObject:@(visibleCellIndexPaths[i].section)]; - } - } - for (NSInteger i = 0; i < visibleSections.count; i++) { - NSInteger section = visibleSections[i].integerValue; + // iOS 14 及以前的版本,只要某个 section 的 header 露出来了,该 section 里的第一个 cell 必定会出现在 visibleRows 内,但 iOS 15 必须是真的显示了 cell 才会出现在 visibleRows 里 + // 这里针对 iOS 15 做个保护:如果最后一个可视 cell 刚好是该 section 的最后一个 cell,则检测范围再扩大到下一个 section,避免遗漏 + NSMutableArray *result = NSMutableArray.new; + NSInteger sections = self.numberOfSections; + for (NSInteger section = 0; section < sections; section++) { if ([self qmui_isHeaderVisibleForSection:section]) { - [result addObject:visibleSections[i]]; + [result addObject:@(section)]; } } if (result.count == 0) { @@ -229,14 +343,13 @@ - (BOOL)qmui_isHeaderPinnedForSection:(NSInteger)section { // 系统这两个接口获取到的 rect 是在 contentSize 里的 rect,而不是实际看到的 rect CGRect rectForSection = [self rectForSection:section]; CGRect rectForHeader = [self rectForHeaderInSection:section]; - BOOL isSectionScrollIntoContentInsetTop = self.contentOffset.y + self.qmui_contentInset.top > CGRectGetMinY(rectForSection);// 表示这个 section 已经往上滚动,超过 contentInset.top 那条线了 - BOOL isSectionStayInContentInsetTop = self.contentOffset.y + self.qmui_contentInset.top <= CGRectGetMaxY(rectForSection) - CGRectGetHeight(rectForHeader);// 表示这个 section 还没被完全滚走 + BOOL isSectionScrollIntoContentInsetTop = self.contentOffset.y + self.adjustedContentInset.top > CGRectGetMinY(rectForHeader);// 表示这个 section 已经往上滚动,超过 contentInset.top 那条线了 + BOOL isSectionStayInContentInsetTop = self.contentOffset.y + self.adjustedContentInset.top <= CGRectGetMaxY(rectForSection) - CGRectGetHeight(rectForHeader);// 表示这个 section 还没被完全滚走 BOOL isPinned = isSectionScrollIntoContentInsetTop && isSectionStayInContentInsetTop; return isPinned; } - (BOOL)qmui_isHeaderVisibleForSection:(NSInteger)section { - if (self.style != UITableViewStylePlain) return NO; if (section >= [self numberOfSections]) return NO; // 不存在 header 就不用判断 @@ -244,11 +357,17 @@ - (BOOL)qmui_isHeaderVisibleForSection:(NSInteger)section { if (CGRectGetHeight(rectForSectionHeader) <= 0) return NO; // 系统这个接口获取到的 rect 是在 contentSize 里的 rect,而不是实际看到的 rect - CGRect rectForSection = [self rectForSection:section]; - BOOL isSectionScrollIntoBounds = CGRectGetMinY(rectForSection) < self.contentOffset.y + CGRectGetHeight(self.bounds); - BOOL isSectionStayInContentInsetTop = self.contentOffset.y + self.qmui_contentInset.top < CGRectGetMaxY(rectForSection);// 表示这个 section 还没被完全滚走 - BOOL isVisible = isSectionScrollIntoBounds && isSectionStayInContentInsetTop; - return isVisible; + CGRect rectForSection = CGRectZero; + if (self.style == UITableViewStylePlain) { + rectForSection = [self rectForSection:section]; + } else { + rectForSection = [self rectForHeaderInSection:section]; + } + CGRect visibleRect = CGRectMake(self.contentOffset.x + self.adjustedContentInset.left, self.contentOffset.y + self.adjustedContentInset.top, CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.adjustedContentInset), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.adjustedContentInset)); + if (CGRectIntersectsRect(visibleRect, rectForSection)) { + return YES; + } + return NO; } - (QMUITableViewCellPosition)qmui_positionForRowAtIndexPath:(NSIndexPath *)indexPath { @@ -303,6 +422,14 @@ - (void)qmui_scrollToRowFittingOffsetY:(CGFloat)offsetY atIndexPath:(NSIndexPath } } +- (CGFloat)qmui_validContentWidth { + CGRect indexFrame = self.qmui_indexFrame; + CGFloat rightInset = MAX(self.safeAreaInsets.right + (self.style == UITableViewStyleInsetGrouped ? self.qmui_insetGroupedHorizontalInset : 0), CGRectGetWidth(indexFrame)); + CGFloat leftInset = self.safeAreaInsets.left + (self.style == UITableViewStyleInsetGrouped ? self.qmui_insetGroupedHorizontalInset : 0); + CGFloat width = CGRectGetWidth(self.bounds) - leftInset - rightInset; + return width; +} + - (CGSize)qmui_realContentSize { [self alertEstimatedHeightUsageIfDetected]; @@ -332,7 +459,7 @@ - (BOOL)qmui_canScroll { } if ([self.tableHeaderView isKindOfClass:[UISearchBar class]]) { - BOOL canScroll = self.qmui_realContentSize.height + UIEdgeInsetsGetVerticalValue(self.qmui_contentInset) > CGRectGetHeight(self.bounds); + BOOL canScroll = self.qmui_realContentSize.height + UIEdgeInsetsGetVerticalValue(self.adjustedContentInset) > CGRectGetHeight(self.bounds); return canScroll; } else { return [super qmui_canScroll]; @@ -340,29 +467,145 @@ - (BOOL)qmui_canScroll { } - (void)alertEstimatedHeightUsageIfDetected { - BOOL usingEstimatedRowHeight = self.estimatedRowHeight == UITableViewAutomaticDimension; - BOOL usingEstimatedSectionHeaderHeight = self.estimatedSectionHeaderHeight == UITableViewAutomaticDimension; - BOOL usingEstimatedSectionFooterHeight = self.estimatedSectionFooterHeight == UITableViewAutomaticDimension; + BOOL usingEstimatedRowHeight = [self.delegate respondsToSelector:@selector(tableView:estimatedHeightForRowAtIndexPath:)] || self.estimatedRowHeight > 0; + BOOL usingEstimatedSectionHeaderHeight = [self.delegate respondsToSelector:@selector(tableView:estimatedHeightForHeaderInSection:)] || self.estimatedSectionHeaderHeight > 0; + BOOL usingEstimatedSectionFooterHeight = [self.delegate respondsToSelector:@selector(tableView:estimatedHeightForFooterInSection:)] || self.estimatedSectionFooterHeight > 0; + if (!IS_DEBUG) return; if (usingEstimatedRowHeight || usingEstimatedSectionHeaderHeight || usingEstimatedSectionFooterHeight) { [self QMUISymbolicUsingTableViewEstimatedHeightMakeWarning]; } } - (void)QMUISymbolicUsingTableViewEstimatedHeightMakeWarning { - QMUILog(@"UITableView 的 estimatedRow(SectionHeader / SectionFooter)Height 属性会影响 contentSize、sizeThatFits:、rectForXxx 等方法的计算,导致计算结果不准确,建议重新考虑是否要使用 estimated。可添加 '%@' 的 Symbolic Breakpoint 以捕捉此类信息\n%@", NSStringFromSelector(_cmd), [NSThread callStackSymbols]); + QMUILog(@"UITableView (QMUI)", @"当开启了 UITableView 的 estimatedRow(SectionHeader / SectionFooter)Height 功能后,不应该手动修改 contentOffset 和 contentSize,也会影响 contentSize、sizeThatFits:、rectForXxx 等方法的计算,请注意确认当前是否存在不合理的业务代码。可添加 'QMUISymbolicUsingTableViewEstimatedHeightMakeWarning' 的 Symbolic Breakpoint 以捕捉此类信息。"); } - (void)qmui_performBatchUpdates:(void (NS_NOESCAPE ^ _Nullable)(void))updates completion:(void (^ _Nullable)(BOOL finished))completion { - if (@available(iOS 11.0, *)) { - [self performBatchUpdates:updates completion:completion]; - } else { - if (!updates && completion) { - completion(YES);// 私有方法对 updates 为空的情况,不会调用 completion,但 iOS 11 新增的方法是可以的,所以这里对齐新版本的行为 - } else { - [self qmui_performSelector:NSSelectorFromString([NSString stringWithFormat:@"_%@BatchUpdates:%@:", @"perform", @"completion"]) withArguments:&updates, &completion, nil]; - } + [self performBatchUpdates:updates completion:completion]; +} + +- (CGRect)qmui_indexFrame { + CGRect indexFrame = CGRectZero; + [self qmui_performSelector:NSSelectorFromString(@"indexFrame") withPrimitiveReturnValue:&indexFrame]; + return indexFrame; +} + +@end + +@interface UITableViewCell (QMUI_Private) + +@property(nonatomic, assign, readwrite) QMUITableViewCellPosition qmui_cellPosition; +@end + +@implementation UITableView (QMUI_InsetGrouped) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // -[UITableViewDelegate tableView:willDisplayCell:forRowAtIndexPath:] 比这个还晚,所以不用担心触发 delegate + OverrideImplementation([UITableView class], NSSelectorFromString(@"_configureCellForDisplay:forIndexPath:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITableView *selfObject, UITableViewCell *cell, NSIndexPath *indexPath) { + + // call super + void (*originSelectorIMP)(id, SEL, UITableViewCell *, NSIndexPath *); + originSelectorIMP = (void (*)(id, SEL, UITableViewCell *, NSIndexPath *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, cell, indexPath); + + // UITableViewCell(QMUI) 内会根据 cellPosition 调整 separator 的布局,所以先在这里赋值以供那边使用 + QMUITableViewCellPosition position = [selfObject qmui_positionForRowAtIndexPath:indexPath]; + cell.qmui_cellPosition = position; + + if (selfObject.style == UITableViewStyleInsetGrouped) { + CGFloat cornerRadius = selfObject.qmui_insetGroupedCornerRadius; + if (position == QMUITableViewCellPositionMiddleInSection || position == QMUITableViewCellPositionNone) { + cornerRadius = 0; + } + cell.layer.cornerRadius = cornerRadius; + } + + if (cell.qmui_configureStyleBlock) { + cell.qmui_configureStyleBlock(selfObject, cell, indexPath); + } + }; + }); + + // -[UITableViewCell _setContentClipCorners:updateCorners:],用来控制系统 InsetGrouped 的圆角(很多情况都会触发系统更新圆角,例如设置 cell.backgroundColor、...,对于 iOS 12 及以下的系统,则靠 -[UITableView _configureCellForDisplay:forIndexPath:] 来处理 + // - (void) _setContentClipCorners:(unsigned long)arg1 updateCorners:(BOOL)arg2; (0x10db0a5b7) + OverrideImplementation([UITableViewCell class], NSSelectorFromString([NSString qmui_stringByConcat:@"_setContentClipCorners", @":", @"updateCorners", @":", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITableViewCell *selfObject, CACornerMask firstArgv, BOOL secondArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, CACornerMask, BOOL); + originSelectorIMP = (void (*)(id, SEL, CACornerMask, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); + + UITableView *tableView = selfObject.qmui_tableView; + if (tableView && tableView.style == UITableViewStyleInsetGrouped) { + CGFloat cornerRadius = tableView.qmui_insetGroupedCornerRadius; + if (selfObject.qmui_cellPosition == QMUITableViewCellPositionMiddleInSection || selfObject.qmui_cellPosition == QMUITableViewCellPositionNone) { + cornerRadius = 0; + } + selfObject.layer.cornerRadius = cornerRadius; + } + }; + }); + + + // -[UITableView layoutMargins],用来控制系统 InsetGrouped 的左右间距 + OverrideImplementation([UITableView class], @selector(layoutMargins), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIEdgeInsets(UITableView *selfObject) { + // call super + UIEdgeInsets (*originSelectorIMP)(id, SEL); + originSelectorIMP = (UIEdgeInsets (*)(id, SEL))originalIMPProvider(); + UIEdgeInsets result = originSelectorIMP(selfObject, originCMD); + + if (selfObject.style == UITableViewStyleInsetGrouped) { + result.left = selfObject.safeAreaInsets.left + selfObject.qmui_insetGroupedHorizontalInset; + result.right = selfObject.safeAreaInsets.right + selfObject.qmui_insetGroupedHorizontalInset; + } + + return result; + }; + }); + }); +} + +static char kAssociatedObjectKey_insetGroupedCornerRadius; +- (void)setQmui_insetGroupedCornerRadius:(CGFloat)qmui_insetGroupedCornerRadius { + objc_setAssociatedObject(self, &kAssociatedObjectKey_insetGroupedCornerRadius, @(qmui_insetGroupedCornerRadius), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (self.style == UITableViewStyleInsetGrouped && self.indexPathsForVisibleRows.count) { + [self reloadData]; + } +} + +- (CGFloat)qmui_insetGroupedCornerRadius { + NSNumber *associatedValue = (NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_insetGroupedCornerRadius); + if (!associatedValue) { + // 从来没设置过(包括业务主动设置或者通过 UIAppearance 方式设置),则用 iOS 13 系统默认值 + // 不在 UITableView init 时设置是因为那样会使 UIAppearance 失效 + return 10; + } + return associatedValue.qmui_CGFloatValue; +} + +static char kAssociatedObjectKey_insetGroupedHorizontalInset; +- (void)setQmui_insetGroupedHorizontalInset:(CGFloat)qmui_insetGroupedHorizontalInset { + objc_setAssociatedObject(self, &kAssociatedObjectKey_insetGroupedHorizontalInset, @(qmui_insetGroupedHorizontalInset), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (self.style == UITableViewStyleInsetGrouped && self.indexPathsForVisibleRows.count) { + [self reloadData]; + } +} + +- (CGFloat)qmui_insetGroupedHorizontalInset { + NSNumber *associatedValue = (NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_insetGroupedHorizontalInset); + if (!associatedValue) { + // 从来没设置过(包括业务主动设置或者通过 UIAppearance 方式设置),则用 iOS 13 系统默认值 + // 不在 UITableView init 时设置是因为那样会使 UIAppearance 失效 + return PreferredValueForVisualDevice(20, 15); } + return associatedValue.qmui_CGFloatValue; } @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UITableViewCell+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UITableViewCell+QMUI.h index 01a39e7a..be562302 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITableViewCell+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UITableViewCell+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITableViewCell+QMUI.h @@ -14,16 +14,87 @@ // #import +#import "UITableView+QMUI.h" + +NS_ASSUME_NONNULL_BEGIN + +/// 用于在 @c qmui_separatorInsetsBlock @c qmui_topSeparatorInsetsBlock 里作为”不需要分隔线“的标志返回 +extern const UIEdgeInsets QMUITableViewCellSeparatorInsetsNone; @interface UITableViewCell (QMUI) +/// 获取当前 cell 所在的 tableView,iOS 13 下在 cellForRow(heightForRow 内不可以) 内 init 完 cell 就可以获取到值,而 iOS 12 及以下只能在 cell 即将显示时(也即 willDisplay 之前)才能获取到值 +@property(nonatomic, weak, readonly, nullable) UITableView *qmui_tableView; + +/// 当 cell 内部可以访问到 tableView 时就会调用这个 block,内部会做过滤,tableView 指针不变就不会再调用 +/// @note 一般情况下 iOS 13 及以后的版本,cellForRow 里的 cell init 完立马就可以访问到 tableView 了,而其他低版本要等到 willDisplayCell 之前才可以访问到。 +@property(nonatomic, copy, nullable) void (^qmui_didAddToTableViewBlock)(__kindof UITableView *tableView, __kindof UITableViewCell *cell); + +/// 获取当前 cell 初始化时用的 style 值 +@property(nonatomic, assign, readonly) UITableViewCellStyle qmui_style; + +/// cell 在当前 section 里的位置,在 willDisplayCell 时可以使用,cellForRow 里只能自己使用 -[UITableView qmui_positionForRowAtIndexPath:] 获取。 +@property(nonatomic, assign, readonly) QMUITableViewCellPosition qmui_cellPosition; + +/** + 设置 cell 的样式(不影响 cell 高度的那些,例如各种颜色、圆角等),会在 willDisplayCell 之前被调用,在 block 被调用时已经能拿到 tableView 的引用,所以便于根据 tableView 的不同属性来配置 cell 不同的外观(例如同一个 cell 被分别用于 Plain、Grouped 的列表时要展示不一样的外观)。亦可以通过 cell.qmui_cellPosition 得到 cell 在 section 里的位置。 + @note 该 block 可能会不断调用(参考 UITableViewDelegate willDisplayCell),注意不要在里面做耗时操作。 + */ +@property(nonatomic, copy, nullable) void (^qmui_configureStyleBlock)(__kindof UITableView *tableView, __kindof UITableViewCell *cell, NSIndexPath * _Nullable indexPath); + +/** + 设置 cell 在 moveRow 时的样式(系统默认是 0.8 alpha + 上下很重的投影,且无法自定义),在 cellForRow 里设置后会在每次 move 的开始、结束时触发。通常可利用这个 block 修改 cell.qmui_shadow 样式。 + + @warning 当 cell 的这个 block 值非空时,该 cell 在 moveRow 过程中会强制设置 clipsToBounds 为 NO、alpha 为1,且移除系统添加的投影(系统的投影是 UITableView 的 subview,不是 cell 的),所以即便你设置了这个 block 但里面什么都不做,也会导致该 cell 的系统默认样式丢失。 + */ +@property(nonatomic, copy, nullable) void (^qmui_configureReorderingStyleBlock)(__kindof UITableView *tableView, __kindof UITableViewCell *aCell, BOOL isReordering); + +/** + 控制 cell 的分隔线位置,做成 block 的形式是为了方便根据不同的 UITableViewStyle 以及不同的 QMUITableViewCellPosition (通过 cell.qmui_cellPosition 获取)来设置不同的分隔线缩进。分隔线默认是左右撑满整个 cell 的,通过这个 block 返回一个 insets 则会基于整个 cell 的宽度减去 insets 的值得到最终分隔线的布局,如果某些位置不需要分隔线可以返回 QMUITableViewCellSeparatorInsetsNone。 + + @note 只有在 tableView.separatorStyle != UITableViewCellSeparatorStyleNone 时才会出现分隔线,而分隔线的颜色则由 tableView.separatorColor 控制。创建这个属性的背景是当你希望用 UITableView 系统提供的接口去控制分隔线显隐时,会发现很难调整每个 cell 内的分隔线位置及显示/隐藏逻辑(例如最后一个 cell 不要分隔线),此时你可以用这个属性来达到自定义的目的。当 block 不为空时,内部实际上会创建一条自定义的分隔线来代替系统的,系统自带的分隔线会被隐藏。 + + @warning 注意分隔线是放在 cell 上的,而 cell.textLabel 等 subviews 是放在 cell.contentView 上的,所以如果分隔线要参照其他 subviews 布局的话,要注意坐标系转换。 + */ +@property(nonatomic, copy, nullable) UIEdgeInsets (^qmui_separatorInsetsBlock)(__kindof UITableView *tableView, __kindof UITableViewCell *cell); + +/** + 控制 cell 的顶部分隔线位置,其他信息参考 @c qmui_separatorInsetsBlock + */ +@property(nonatomic, copy, nullable) UIEdgeInsets (^qmui_topSeparatorInsetsBlock)(__kindof UITableView *tableView, __kindof UITableViewCell *cell); + +/// 设置 cell 点击时的背景色,如果没有 selectedBackgroundView 会创建一个。 +/// @warning 请勿再使用 self.selectedBackgroundView.backgroundColor 修改,因为 QMUITheme 里会重新应用 qmui_selectedBackgroundColor,会覆盖 self.selectedBackgroundView.backgroundColor 的效果。 +@property(nonatomic, strong, nullable) UIColor *qmui_selectedBackgroundColor; + /// setHighlighted:animated: 方法的回调 block -@property(nonatomic, copy) void (^qmui_setHighlightedBlock)(BOOL highlighted, BOOL animated); +@property(nonatomic, copy, nullable) void (^qmui_setHighlightedBlock)(BOOL highlighted, BOOL animated); /// setSelected:animated: 方法的回调 block -@property(nonatomic, copy) void (^qmui_setSelectedBlock)(BOOL selected, BOOL animated); +@property(nonatomic, copy, nullable) void (^qmui_setSelectedBlock)(BOOL selected, BOOL animated); + +/** + 获取当前 cell 的 accessoryView,优先级分别是:当前肉眼可视的 view(比如进入排序模式时的 reorderControl) ->编辑状态下的 editingAccessoryView -> 编辑状态下的系统自己的 accessoryView -> 普通状态下的自定义 accessoryView -> 普通状态下系统自己的 accessoryView。 + + @note 对于系统的 UITableViewCellAccessoryDetailDisclosureButton,iOS 12 及以下是一个 UITableViewCellDetailDisclosureView,而 iOS 13 及以上被拆成两个独立的 view,此时 qmui_accessoryView 只能返回布局上更靠左的那个 view。 + 如果你给 cell 设置了自己的 accessoryView,但此时 cell 进入排序模式,系统会把你的 accessoryView 隐藏掉,强制显示为 reorderControl,此时 UITableViewCell.accessoryView 返回的是你自己设置的 view,而 UITableViewCell.qmui_accessoryView 返回的是当前可视的 view(也即 reorderControl)。 + + @warning 一般在 willDisplayCell 里使用,cellForRow 里可能太早了很多 view 尚未被创建,会返回 nil +*/ +@property(nonatomic, strong, readonly, nullable) __kindof UIView *qmui_accessoryView; -/// 获取当前 cell 的 accessoryView,优先级分别是:编辑状态下的 editingAccessoryView -> 编辑状态下的系统自己的 accessoryView -> 普通状态下的自定义 accessoryView -> 普通状态下系统自己的 accessoryView -@property(nonatomic, strong, readonly) __kindof UIView *qmui_accessoryView; +@end + +@interface UITableViewCell (QMUI_Styled) +/// 按照 QMUI 配置表的值来将 cell 设置为全局统一的样式 +- (void)qmui_styledAsQMUITableViewCell; + +@property(nonatomic, strong, readonly, nullable) UIColor *qmui_styledTextLabelColor; +@property(nonatomic, strong, readonly, nullable) UIColor *qmui_styledDetailTextLabelColor; +@property(nonatomic, strong, readonly, nullable) UIColor *qmui_styledBackgroundColor; +@property(nonatomic, strong, readonly, nullable) UIColor *qmui_styledSelectedBackgroundColor; +@property(nonatomic, strong, readonly, nullable) UIColor *qmui_styledWarningBackgroundColor; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UITableViewCell+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UITableViewCell+QMUI.m index 77ddd9bb..e482082b 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITableViewCell+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UITableViewCell+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITableViewCell+QMUI.m @@ -14,18 +14,50 @@ // #import "UITableViewCell+QMUI.h" -#import #import "QMUICore.h" +#import "UIView+QMUI.h" +#import "UITableView+QMUI.h" +#import "CALayer+QMUI.h" + +const UIEdgeInsets QMUITableViewCellSeparatorInsetsNone = {INFINITY, INFINITY, INFINITY, INFINITY}; + +@interface UITableViewCell () + +@property(nonatomic, copy) NSString *qmuiTbc_cachedAddToTableViewBlockKey; +@property(nonatomic, strong) CALayer *qmuiTbc_separatorLayer; +@property(nonatomic, strong) CALayer *qmuiTbc_topSeparatorLayer; +@end @implementation UITableViewCell (QMUI) +QMUISynthesizeNSIntegerProperty(qmui_style, setQmui_style) +QMUISynthesizeIdCopyProperty(qmuiTbc_cachedAddToTableViewBlockKey, setQmuiTbc_cachedAddToTableViewBlockKey) +QMUISynthesizeIdCopyProperty(qmui_configureStyleBlock, setQmui_configureStyleBlock) +QMUISynthesizeIdStrongProperty(qmuiTbc_separatorLayer, setQmuiTbc_separatorLayer) +QMUISynthesizeIdStrongProperty(qmuiTbc_topSeparatorLayer, setQmuiTbc_topSeparatorLayer) +QMUISynthesizeIdCopyProperty(qmui_separatorInsetsBlock, setQmui_separatorInsetsBlock) +QMUISynthesizeIdCopyProperty(qmui_topSeparatorInsetsBlock, setQmui_topSeparatorInsetsBlock) QMUISynthesizeIdCopyProperty(qmui_setHighlightedBlock, setQmui_setHighlightedBlock) QMUISynthesizeIdCopyProperty(qmui_setSelectedBlock, setQmui_setSelectedBlock) -+ (void)initialize { ++ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - + OverrideImplementation([UITableViewCell class], @selector(initWithStyle:reuseIdentifier:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UITableViewCell *(UITableViewCell *selfObject, UITableViewCellStyle firstArgv, NSString *secondArgv) { + // call super + UITableViewCell *(*originSelectorIMP)(id, SEL, UITableViewCellStyle, NSString *); + originSelectorIMP = (UITableViewCell *(*)(id, SEL, UITableViewCellStyle, NSString *))originalIMPProvider(); + UITableViewCell *result = originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); + + // 系统虽然有私有 API - (UITableViewCellStyle)style; 可以用,但该方法在 init 内得到的永远是 0,只有 init 执行完成后才可以得到正确的值,所以这里只能自己记录 + result.qmui_style = firstArgv; + + [selfObject qmuiTbc_callAddToTableViewBlockIfCan]; + + return result; + }; + }); ExtendImplementationOfVoidMethodWithTwoArguments([UITableViewCell class], @selector(setHighlighted:animated:), BOOL, BOOL, ^(UITableViewCell *selfObject, BOOL highlighted, BOOL animated) { if (selfObject.qmui_setHighlightedBlock) { selfObject.qmui_setHighlightedBlock(highlighted, animated); @@ -37,20 +69,439 @@ + (void)initialize { selfObject.qmui_setSelectedBlock(selected, animated); } }); + + // 修复 iOS 13.0 UIButton 作为 cell.accessoryView 时布局错误的问题 + // https://github.com/Tencent/QMUI_iOS/issues/693 + if (@available(iOS 13.1, *)) { + } else { + ExtendImplementationOfVoidMethodWithoutArguments([UITableViewCell class], @selector(layoutSubviews), ^(UITableViewCell *selfObject) { + if ([selfObject.accessoryView isKindOfClass:[UIButton class]]) { + CGFloat defaultRightMargin = 15 + SafeAreaInsetsConstantForDeviceWithNotch.right; + selfObject.accessoryView.qmui_left = selfObject.qmui_width - defaultRightMargin - selfObject.accessoryView.qmui_width; + selfObject.accessoryView.qmui_top = CGRectGetMinYVerticallyCenterInParentRect(selfObject.frame, selfObject.accessoryView.frame);; + selfObject.contentView.qmui_right = selfObject.accessoryView.qmui_left; + } + }); + } + + OverrideImplementation([UITableViewCell class], NSSelectorFromString(@"_setTableView:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITableViewCell *selfObject, UITableView *firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, UITableView *); + originSelectorIMP = (void (*)(id, SEL, UITableView *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + [selfObject qmuiTbc_callAddToTableViewBlockIfCan]; + }; + }); }); } +static char kAssociatedObjectKey_cellPosition; +- (void)setQmui_cellPosition:(QMUITableViewCellPosition)qmui_cellPosition { + objc_setAssociatedObject(self, &kAssociatedObjectKey_cellPosition, @(qmui_cellPosition), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + BOOL shouldShowSeparatorInTableView = self.qmui_tableView && self.qmui_tableView.separatorStyle != UITableViewCellSeparatorStyleNone; + if (shouldShowSeparatorInTableView) { + [self qmuiTbc_createSeparatorLayerIfNeeded]; + [self qmuiTbc_createTopSeparatorLayerIfNeeded]; + } else { + self.qmuiTbc_separatorLayer.hidden = YES; + self.qmuiTbc_topSeparatorLayer.hidden = YES; + } +} + +- (QMUITableViewCellPosition)qmui_cellPosition { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_cellPosition)) integerValue]; +} + +static char kAssociatedObjectKey_didAddToTableViewBlock; +- (void)setQmui_didAddToTableViewBlock:(void (^)(__kindof UITableView * _Nonnull, __kindof UITableViewCell * _Nonnull))qmui_didAddToTableViewBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_didAddToTableViewBlock, qmui_didAddToTableViewBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + [self qmuiTbc_callAddToTableViewBlockIfCan]; +} + +- (void (^)(__kindof UITableView * _Nonnull, __kindof UITableViewCell * _Nonnull))qmui_didAddToTableViewBlock { + return (void (^)(__kindof UITableView * _Nonnull, __kindof UITableViewCell * _Nonnull))objc_getAssociatedObject(self, &kAssociatedObjectKey_didAddToTableViewBlock); +} + +- (void)qmuiTbc_callAddToTableViewBlockIfCan { + if (!self.qmui_tableView || !self.qmui_didAddToTableViewBlock) return; + NSString *key = [NSString stringWithFormat:@"%p%p", self.qmui_tableView, self.qmui_didAddToTableViewBlock]; + if ([key isEqualToString:self.qmuiTbc_cachedAddToTableViewBlockKey]) return; + self.qmui_didAddToTableViewBlock(self.qmui_tableView, self); + self.qmuiTbc_cachedAddToTableViewBlockKey = key; +} + +- (void)qmuiTbc_swizzleLayoutSubviews { + [QMUIHelper executeBlock:^{ + ExtendImplementationOfVoidMethodWithoutArguments(self.class, @selector(layoutSubviews), ^(UITableViewCell *cell) { + if (cell.qmuiTbc_separatorLayer && !cell.qmuiTbc_separatorLayer.hidden) { + UIEdgeInsets insets = cell.qmui_separatorInsetsBlock(cell.qmui_tableView, cell); + CGRect frame = CGRectZero; + if (!UIEdgeInsetsEqualToEdgeInsets(insets, QMUITableViewCellSeparatorInsetsNone)) { + CGFloat height = PixelOne; + frame = CGRectMake(insets.left, CGRectGetHeight(cell.bounds) - height + insets.top - insets.bottom, MAX(0, CGRectGetWidth(cell.bounds) - UIEdgeInsetsGetHorizontalValue(insets)), height); + } + cell.qmuiTbc_separatorLayer.frame = frame; + } + + if (cell.qmuiTbc_topSeparatorLayer && !cell.qmuiTbc_topSeparatorLayer.hidden) { + UIEdgeInsets insets = cell.qmui_topSeparatorInsetsBlock(cell.qmui_tableView, cell); + CGRect frame = CGRectZero; + if (!UIEdgeInsetsEqualToEdgeInsets(insets, QMUITableViewCellSeparatorInsetsNone)) { + CGFloat height = PixelOne; + frame = CGRectMake(insets.left, insets.top - insets.bottom, MAX(0, CGRectGetWidth(cell.bounds) - UIEdgeInsetsGetHorizontalValue(insets)), height); + } + cell.qmuiTbc_topSeparatorLayer.frame = frame; + } + }); + } oncePerIdentifier:[NSString stringWithFormat:@"UITableViewCell %@-%@", NSStringFromClass(self.class), NSStringFromSelector(@selector(layoutSubviews))]]; +} + +- (BOOL)qmuiTbc_customizedSeparator { + return !!self.qmui_separatorInsetsBlock; +} + +- (BOOL)qmuiTbc_customizedTopSeparator { + return !!self.qmui_topSeparatorInsetsBlock; +} + +- (void)qmuiTbc_createSeparatorLayerIfNeeded { + if (![self qmuiTbc_customizedSeparator]) { + self.qmuiTbc_separatorLayer.hidden = YES; + return; + } + + BOOL shouldShowSeparator = !UIEdgeInsetsEqualToEdgeInsets(self.qmui_separatorInsetsBlock(self.qmui_tableView, self), QMUITableViewCellSeparatorInsetsNone); + if (shouldShowSeparator) { + if (!self.qmuiTbc_separatorLayer) { + [self qmuiTbc_swizzleLayoutSubviews]; + self.qmuiTbc_separatorLayer = [CALayer layer]; + [self.qmuiTbc_separatorLayer qmui_removeDefaultAnimations]; + [self.layer addSublayer:self.qmuiTbc_separatorLayer]; + } + self.qmuiTbc_separatorLayer.backgroundColor = self.qmui_tableView.separatorColor.CGColor; + self.qmuiTbc_separatorLayer.hidden = NO; + } else { + if (self.qmuiTbc_separatorLayer) { + self.qmuiTbc_separatorLayer.hidden = YES; + } + } +} + +- (void)qmuiTbc_createTopSeparatorLayerIfNeeded { + if (![self qmuiTbc_customizedTopSeparator]) { + self.qmuiTbc_topSeparatorLayer.hidden = YES; + return; + } + + BOOL shouldShowSeparator = !UIEdgeInsetsEqualToEdgeInsets(self.qmui_topSeparatorInsetsBlock(self.qmui_tableView, self), QMUITableViewCellSeparatorInsetsNone); + if (shouldShowSeparator) { + if (!self.qmuiTbc_topSeparatorLayer) { + [self qmuiTbc_swizzleLayoutSubviews]; + self.qmuiTbc_topSeparatorLayer = [CALayer layer]; + [self.qmuiTbc_topSeparatorLayer qmui_removeDefaultAnimations]; + [self.layer addSublayer:self.qmuiTbc_topSeparatorLayer]; + } + self.qmuiTbc_topSeparatorLayer.backgroundColor = self.qmui_tableView.separatorColor.CGColor; + self.qmuiTbc_topSeparatorLayer.hidden = NO; + } else { + if (self.qmuiTbc_topSeparatorLayer) { + self.qmuiTbc_topSeparatorLayer.hidden = YES; + } + } +} + +- (UITableView *)qmui_tableView { + return [self valueForKey:@"_tableView"]; +} + +static char kAssociatedObjectKey_selectedBackgroundColor; +- (void)setQmui_selectedBackgroundColor:(UIColor *)qmui_selectedBackgroundColor { + objc_setAssociatedObject(self, &kAssociatedObjectKey_selectedBackgroundColor, qmui_selectedBackgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_selectedBackgroundColor) { + // 系统默认的 selectedBackgroundView 是 UITableViewCellSelectedBackground,无法修改自定义背景色,所以改为用普通的 UIView + if (!self.selectedBackgroundView || [NSStringFromClass(self.selectedBackgroundView.class) hasPrefix:@"UITableViewCell"]) { + self.selectedBackgroundView = [[UIView alloc] init]; + } + self.selectedBackgroundView.backgroundColor = qmui_selectedBackgroundColor; + } +} + +- (UIColor *)qmui_selectedBackgroundColor { + return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_selectedBackgroundColor); +} + - (UIView *)qmui_accessoryView { + // 优先获取当前肉眼可见的 view,包括系统的排序、删除、checkbox 等,仅在 willDisplayCell 内有效,cellForRow 太早了拿不到 + BeginIgnorePerformSelectorLeaksWarning + SEL managerSEL = NSSelectorFromString(@"_accessoryManager"); + if ([self respondsToSelector:managerSEL]) { + id manager = [self performSelector:managerSEL]; + NSDictionary *accessoryViews = [manager performSelector:NSSelectorFromString(@"accessoryViews")]; + UIView *view = accessoryViews.allValues.firstObject; + if (view) { + return view; + } + } + EndIgnorePerformSelectorLeaksWarning + if (self.editing) { if (self.editingAccessoryView) { return self.editingAccessoryView; } - return [self valueForKey:@"_editingAccessoryView"]; + return [self qmui_valueForKey:@"_editingAccessoryView"]; } if (self.accessoryView) { return self.accessoryView; } - return [self valueForKey:@"_accessoryView"]; + + // UITableViewCellAccessoryDetailDisclosureButton 在 iOS 13 及以上是分开的两个 accessoryView,以 NSSet 的形式存在这个私有接口里。而 iOS 12 及以下是以一个 UITableViewCellDetailDisclosureView 的 UIControl 存在。 + NSSet *accessoryViews = [self qmui_valueForKey:@"_existingSystemAccessoryViews"]; + if ([accessoryViews isKindOfClass:NSSet.class] && accessoryViews.count) { + UIView *leftView = nil; + for (UIView *accessoryView in accessoryViews) { + if (!leftView) { + leftView = accessoryView; + continue; + } + if (CGRectGetMinX(accessoryView.frame) < CGRectGetMinX(leftView.frame)) { + leftView = accessoryView; + } + } + return leftView; + } + return nil; +} + +static char kAssociatedObjectKey_configureReorderingStyleBlock; +- (void)setQmui_configureReorderingStyleBlock:(void (^)(__kindof UITableView * _Nonnull, __kindof UITableViewCell * _Nonnull, BOOL))configureReorderingStyleBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_configureReorderingStyleBlock, configureReorderingStyleBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (configureReorderingStyleBlock) { + + static NSString *kCellKey = @"QMUI_configureCell"; + + [QMUIHelper executeBlock:^{ + // - [UITableViewCell _setReordering:] + // - (void) _setReordering:(BOOL)arg1; (0x1177b462a) + OverrideImplementation([UITableViewCell class], NSSelectorFromString([NSString qmui_stringByConcat:@"_", @"set", @"Reordering", @":", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITableViewCell *selfObject, BOOL firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if (selfObject.qmui_configureReorderingStyleBlock) { + selfObject.qmui_configureReorderingStyleBlock(selfObject.qmui_tableView, selfObject, firstArgv); + } + }; + }); + + // - [UITableViewCell _shouldMaskToBoundsWhileAnimating] + OverrideImplementation([UITableViewCell class], NSSelectorFromString([NSString qmui_stringByConcat:@"_", @"should", @"MaskToBounds", @"WhileAnimating", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^BOOL(UITableViewCell *selfObject) { + // call super + BOOL (*originSelectorIMP)(id, SEL); + originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider(); + BOOL result = originSelectorIMP(selfObject, originCMD); + + // 系统默认在做 move 动作时 cell 是 clip 的,会导致 cell.layer.shadow 不可用,所以强制取消 clip + if (selfObject.qmui_configureReorderingStyleBlock) { + return NO; + } + + return result; + }; + }); + + Class constants = NSClassFromString([NSString qmui_stringByConcat:@"UITable", @"Constants", @"_", @"IOS"]); + if (@available(iOS 14.0, *)) { + + // - [UITableViewCell _setConstants:] + // - (void) _setConstants:(id)arg1; (0x10c36d360) + OverrideImplementation([UITableViewCell class], NSSelectorFromString([NSString qmui_stringByConcat:@"_", @"set", @"Constants", @":", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITableViewCell *selfObject, NSObject *firstArgv) { + + [firstArgv qmui_bindObjectWeakly:selfObject forKey:kCellKey]; + + // call super + void (*originSelectorIMP)(id, SEL, NSObject *); + originSelectorIMP = (void (*)(id, SEL, NSObject *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + }; + }); + + // - [UITableConstants_IOS defaultAlphaForReorderingCell] + OverrideImplementation(constants, NSSelectorFromString([NSString qmui_stringByConcat:@"default", @"Alpha", @"ForReorderingCell", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGFloat(NSObject *selfObject) { + // call super + CGFloat (*originSelectorIMP)(id, SEL); + originSelectorIMP = (CGFloat (*)(id, SEL))originalIMPProvider(); + CGFloat result = originSelectorIMP(selfObject, originCMD); + + UITableViewCell *cell = [selfObject qmui_getBoundObjectForKey:kCellKey]; + if (cell.qmui_configureReorderingStyleBlock) { + return 1; + } + return result; + }; + }); + + // - (BOOL) reorderingCellWantsShadows; (0x109f44dbc) + OverrideImplementation(constants, NSSelectorFromString([NSString qmui_stringByConcat:@"reordering", @"Cell", @"WantsShadows", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^BOOL(NSObject *selfObject) { + // call super + BOOL (*originSelectorIMP)(id, SEL); + originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider(); + BOOL result = originSelectorIMP(selfObject, originCMD); + + UITableViewCell *cell = [selfObject qmui_getBoundObjectForKey:kCellKey]; + if (cell.qmui_configureReorderingStyleBlock) { + return NO; + } + return result; + }; + }); + + } else { + + // - (double) defaultAlphaForReorderingCell:(id)arg1 inTableView:(id)arg2; (0x1174286d7) + OverrideImplementation(constants, NSSelectorFromString([NSString qmui_stringByConcat:@"default", @"Alpha", @"ForReorderingCell:", @"inTableView:", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGFloat(NSObject *selfObject, UITableViewCell *cell, UITableView *tableView) { + + // call super + CGFloat (*originSelectorIMP)(id, SEL, UITableViewCell *, UITableView *); + originSelectorIMP = (CGFloat (*)(id, SEL, UITableViewCell *, UITableView *))originalIMPProvider(); + CGFloat result = originSelectorIMP(selfObject, originCMD, cell, tableView); + + if (cell.qmui_configureReorderingStyleBlock) { + return 1; + } + + return result; + }; + }); + + // - (BOOL) reorderingCellWantsShadows:(id)arg1 inTableView:(id)arg2; (0x1155d86e5) + OverrideImplementation(constants, NSSelectorFromString([NSString qmui_stringByConcat:@"reordering", @"Cell", @"WantsShadows:", @"inTableView:", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^BOOL(NSObject *selfObject, UITableViewCell *cell, UITableView *tableView) { + + // call super + BOOL (*originSelectorIMP)(id, SEL, UITableViewCell *, UITableView *); + originSelectorIMP = (BOOL (*)(id, SEL, UITableViewCell *, UITableView *))originalIMPProvider(); + BOOL result = originSelectorIMP(selfObject, originCMD, cell, tableView); + + if (cell.qmui_configureReorderingStyleBlock) { + return NO; + } + + return result; + }; + }); + } + + + } oncePerIdentifier:@"QMUI_configureReordering"]; + } +} + +- (void (^)(__kindof UITableView * _Nonnull, __kindof UITableViewCell * _Nonnull, BOOL))qmui_configureReorderingStyleBlock { + return (void (^)(__kindof UITableView * _Nonnull, __kindof UITableViewCell * _Nonnull, BOOL))objc_getAssociatedObject(self, &kAssociatedObjectKey_configureReorderingStyleBlock); +} + +@end + +@implementation UITableViewCell (QMUI_Styled) + +- (void)qmui_styledAsQMUITableViewCell { + if (!QMUICMIActivated) return; + + self.textLabel.font = UIFontMake(16); + self.textLabel.backgroundColor = UIColorClear; + UIColor *textLabelColor = self.qmui_styledTextLabelColor; + if (textLabelColor) { + self.textLabel.textColor = textLabelColor; + } + + self.detailTextLabel.font = UIFontMake(15); + self.detailTextLabel.backgroundColor = UIColorClear; + UIColor *detailLabelColor = self.qmui_styledDetailTextLabelColor; + if (detailLabelColor) { + self.detailTextLabel.textColor = detailLabelColor; + } + + UIColor *backgroundColor = self.qmui_styledBackgroundColor; + if (backgroundColor) { + self.backgroundColor = backgroundColor; + } + + UIColor *selectedBackgroundColor = self.qmui_styledSelectedBackgroundColor; + if (selectedBackgroundColor) { + self.qmui_selectedBackgroundColor = selectedBackgroundColor; + } +} + +- (UIColor *)qmui_styledTextLabelColor { + return PreferredValueForTableViewStyle(self.qmui_tableView.style, TableViewCellTitleLabelColor, TableViewGroupedCellTitleLabelColor, TableViewInsetGroupedCellTitleLabelColor); +} + +- (UIColor *)qmui_styledDetailTextLabelColor { + return PreferredValueForTableViewStyle(self.qmui_tableView.style, TableViewCellDetailLabelColor, TableViewGroupedCellDetailLabelColor, TableViewInsetGroupedCellDetailLabelColor); +} + +- (UIColor *)qmui_styledBackgroundColor { + return PreferredValueForTableViewStyle(self.qmui_tableView.style, TableViewCellBackgroundColor, TableViewGroupedCellBackgroundColor, TableViewInsetGroupedCellBackgroundColor); +} + +- (UIColor *)qmui_styledSelectedBackgroundColor { + return PreferredValueForTableViewStyle(self.qmui_tableView.style, TableViewCellSelectedBackgroundColor, TableViewGroupedCellSelectedBackgroundColor, TableViewInsetGroupedCellSelectedBackgroundColor); +} + +- (UIColor *)qmui_styledWarningBackgroundColor { + return PreferredValueForTableViewStyle(self.qmui_tableView.style, TableViewCellWarningBackgroundColor, TableViewGroupedCellWarningBackgroundColor, TableViewInsetGroupedCellWarningBackgroundColor); +} + +@end + +@implementation UITableViewCell (QMUI_InsetGrouped) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + OverrideImplementation([UITableViewCell class], NSSelectorFromString(@"_separatorFrame"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGRect(UITableViewCell *selfObject) { + + if ([selfObject qmuiTbc_customizedSeparator]) { + return CGRectZero; + } + + // call super + CGRect (*originSelectorIMP)(id, SEL); + originSelectorIMP = (CGRect (*)(id, SEL))originalIMPProvider(); + CGRect result = originSelectorIMP(selfObject, originCMD); + return result; + }; + }); + + OverrideImplementation([UITableViewCell class], NSSelectorFromString(@"_topSeparatorFrame"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGRect(UITableViewCell *selfObject) { + + if ([selfObject qmuiTbc_customizedTopSeparator]) { + return CGRectZero; + } + + // call super + CGRect (*originSelectorIMP)(id, SEL); + originSelectorIMP = (CGRect (*)(id, SEL))originalIMPProvider(); + CGRect result = originSelectorIMP(selfObject, originCMD); + return result; + }; + }); + }); } @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UITableViewHeaderFooterView+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UITableViewHeaderFooterView+QMUI.h new file mode 100644 index 00000000..ad2ff052 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UITableViewHeaderFooterView+QMUI.h @@ -0,0 +1,25 @@ +/** + * 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. + */ + +// +// UITableViewHeaderFooterView+QMUI.h +// QMUIKit +// +// Created by MoLice on 2020/6/4. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UITableViewHeaderFooterView (QMUI) + +@property(nonatomic, weak, readonly) UITableView *qmui_tableView; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UITableViewHeaderFooterView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UITableViewHeaderFooterView+QMUI.m new file mode 100644 index 00000000..3cccd2a1 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UITableViewHeaderFooterView+QMUI.m @@ -0,0 +1,27 @@ +/** + * 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. + */ + +// +// UITableViewHeaderFooterView+QMUI.m +// QMUIKit +// +// Created by MoLice on 2020/6/4. +// + +#import "UITableViewHeaderFooterView+QMUI.h" +#import "QMUICore.h" +#import "UITableView+QMUI.h" +#import "UIView+QMUI.h" + +@implementation UITableViewHeaderFooterView (QMUI) + +- (UITableView *)qmui_tableView { + return [self valueForKey:@"tableView"]; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UITextField+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UITextField+QMUI.h index a1ae1fe2..5d4ced27 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITextField+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UITextField+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITextField+QMUI.h @@ -16,15 +16,34 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + @interface UITextField (QMUI) -/// UITextField只有selectedTextRange属性(在协议里定义),这里拓展了一个方法可以将UITextRange类型的selectedTextRange转换为NSRange类型的selectedRange -@property(nonatomic, assign, readonly) NSRange qmui_selectedRange; +/// 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; /// 输入框右边的 clearButton,在 UITextField 初始化后就存在 @property(nullable, nonatomic, weak, readonly) UIButton *qmui_clearButton; -/// 自定义 clearButton 的图片,注意如果设置过则无法再恢复到系统默认的图片 -@property(nonnull, nonatomic, strong) UIImage *qmui_clearButtonImage UI_APPEARANCE_SELECTOR; +/// 自定义 clearButton 的图片,设置成nil,恢复到系统默认的图片 +@property(nullable, nonatomic, strong) UIImage *qmui_clearButtonImage UI_APPEARANCE_SELECTOR; + +/** + * convert UITextRange to NSRange, for example, [self qmui_convertNSRangeFromUITextRange:self.markedTextRange] + */ +- (NSRange)qmui_convertNSRangeFromUITextRange:(UITextRange *)textRange; + +/** + * convert NSRange to UITextRange + * @return return nil if range is invalidate. + */ +- (nullable UITextRange *)qmui_convertUITextRangeFromNSRange:(NSRange)range; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UITextField+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UITextField+QMUI.m index 7c7357cc..3ac2ea59 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITextField+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UITextField+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITextField+QMUI.m @@ -14,25 +14,60 @@ // #import "UITextField+QMUI.h" -#import +#import "NSObject+QMUI.h" +#import "QMUICore.h" +#import "UIImage+QMUI.h" @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]; +} + - (NSRange)qmui_selectedRange { - NSInteger location = [self offsetFromPosition:self.beginningOfDocument toPosition:self.selectedTextRange.start]; - NSInteger length = [self offsetFromPosition:self.selectedTextRange.start toPosition:self.selectedTextRange.end]; - return NSMakeRange(location, length); + return [self qmui_convertNSRangeFromUITextRange:self.selectedTextRange]; } - (UIButton *)qmui_clearButton { - return [self valueForKey:@"clearButton"]; + return [self qmui_valueForKey:@"clearButton"]; } static char kAssociatedObjectKey_clearButtonImage; - (void)setQmui_clearButtonImage:(UIImage *)qmui_clearButtonImage { - if (qmui_clearButtonImage) { - objc_setAssociatedObject(self, &kAssociatedObjectKey_clearButtonImage, qmui_clearButtonImage, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + objc_setAssociatedObject(self, &kAssociatedObjectKey_clearButtonImage, qmui_clearButtonImage, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (@available(iOS 14.0, *)) { [self.qmui_clearButton setImage:qmui_clearButtonImage forState:UIControlStateNormal]; + // 如果当前 clearButton 正在显示的时候把自定义图片去掉,需要重新 layout 一次才能让系统默认图片显示出来 + if (!qmui_clearButtonImage) { + [self setNeedsLayout]; + } } } @@ -40,4 +75,48 @@ - (UIImage *)qmui_clearButtonImage { return (UIImage *)objc_getAssociatedObject(self, &kAssociatedObjectKey_clearButtonImage); } +- (NSRange)qmui_convertNSRangeFromUITextRange:(UITextRange *)textRange { + NSInteger location = [self offsetFromPosition:self.beginningOfDocument toPosition:textRange.start]; + NSInteger length = [self offsetFromPosition:textRange.start toPosition:textRange.end]; + return NSMakeRange(location, length); +} + +- (UITextRange *)qmui_convertUITextRangeFromNSRange:(NSRange)range { + if (range.location == NSNotFound || NSMaxRange(range) > self.text.length) { + return nil; + } + UITextPosition *beginning = self.beginningOfDocument; + UITextPosition *startPosition = [self positionFromPosition:beginning offset:range.location]; + UITextPosition *endPosition = [self positionFromPosition:beginning offset:NSMaxRange(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/QMUI/QMUIKit/UIKitExtensions/UITextInputTraits+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UITextInputTraits+QMUI.h new file mode 100644 index 00000000..31fdb9fb --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UITextInputTraits+QMUI.h @@ -0,0 +1,28 @@ +/** + * 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. + */ +// +// UITextInputTraits+QMUI.h +// QMUIKit +// +// Created by MoLice on 2019/O/16. +// + +#import + +@interface NSObject (QMUITextInput) + +@end + +@interface NSObject (QMUITextInput_Private) + +/// 内部使用,标记某次 keyboardAppearance 的改动是由于 UIView+QMUITheme 内导致的,而非用户手动修改 +@property(nonatomic, assign) UIKeyboardAppearance qmui_keyboardAppearance; + +/// 内部使用,用于标志业务自己修改了 keyboardAppearance 的情况 +@property(nonatomic, assign) BOOL qmui_hasCustomizedKeyboardAppearance; +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UITextInputTraits+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UITextInputTraits+QMUI.m new file mode 100644 index 00000000..4fc1b9b1 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UITextInputTraits+QMUI.m @@ -0,0 +1,109 @@ +/** + * 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. + */ +// +// UITextInputTraits+QMUI.m +// QMUIKit +// +// Created by MoLice on 2019/O/16. +// + +#import "UITextInputTraits+QMUI.h" +#import "QMUICore.h" + +@interface NSObject () + +@property(nonatomic, assign) BOOL qti_didInitialize; +@property(nonatomic, assign) BOOL qti_setKeyboardAppearanceByQMUITheme; +@end + +@implementation NSObject (QMUITextInput) + +QMUISynthesizeBOOLProperty(qti_didInitialize, setQti_didInitialize) +QMUISynthesizeBOOLProperty(qti_setKeyboardAppearanceByQMUITheme, setQti_setKeyboardAppearanceByQMUITheme) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + static NSArray *inputClasses = nil; + if (!inputClasses) inputClasses = @[UITextField.class, UITextView.class, UISearchBar.class]; + [inputClasses enumerateObjectsUsingBlock:^(Class _Nonnull inputClass, NSUInteger idx, BOOL * _Nonnull stop) { + + OverrideImplementation(inputClass, @selector(initWithFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIView *(UIView *selfObject, CGRect firstArgv) { + + // call super + UIView * (*originSelectorIMP)(id, SEL, CGRect); + originSelectorIMP = (UIView * (*)(id, SEL, CGRect))originalIMPProvider(); + UIView * result = originSelectorIMP(selfObject, originCMD, firstArgv); + + if ([selfObject isKindOfClass:NSClassFromString(@"TUIEmojiSearchTextField")]) { + // https://github.com/Tencent/QMUI_iOS/issues/1042 iOS 14 开始,系统的 emoji 键盘内部有一个搜索框 TUIEmojiSearchTextField,这个搜索框如果在 init 的时候设置 keyboardAppearance 会导致再次创建触发死循环,在这里过滤掉它 + // 另外它属于 emoji 键盘内部的 TextFied,其 keyboardAppearance 应该由业务的 UITextField、UITextView 驱动,因此 QMUI 也不应该去干预他 + return result; + } + if (QMUICMIActivated) selfObject.keyboardAppearance = KeyboardAppearance; + selfObject.qti_didInitialize = YES; + return result; + }; + }); + + OverrideImplementation([inputClasses class], @selector(initWithCoder:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIView *(UIView *selfObject, NSCoder *firstArgv) { + + // call super + UIView * (*originSelectorIMP)(id, SEL, NSCoder *); + originSelectorIMP = (UIView * (*)(id, SEL, NSCoder *))originalIMPProvider(); + UIView * result = originSelectorIMP(selfObject, originCMD, firstArgv); + result.qti_didInitialize = YES; + return result; + }; + }); + + // 当输入框聚焦并显示了键盘的情况下,keyboardAppearance 发生变化了,立即刷新键盘的外观 + OverrideImplementation(inputClass, @selector(setKeyboardAppearance:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, UIKeyboardAppearance keyboardAppearance) { + + BOOL valueChanged = selfObject.keyboardAppearance != keyboardAppearance; + + // call super + void (*originSelectorIMP)(id, SEL, UIKeyboardAppearance); + originSelectorIMP = (void (*)(id, SEL, UIKeyboardAppearance))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, keyboardAppearance); + + if (selfObject.qti_didInitialize && valueChanged) { + // 标志当前输入框希望有与配置表不一样的值,则在 QMUITheme 发生变化时不要替它自动切换 + if (QMUICMIActivated && !selfObject.qti_setKeyboardAppearanceByQMUITheme) selfObject.qmui_hasCustomizedKeyboardAppearance = YES; + + // 是否需要立即刷新外观是不需要考虑当前是否为 isFristResponder 的,因为 reloadInputViews 内部会自行处理 + [selfObject reloadInputViews]; + } + }; + }); + }]; + }); +} + +@end + +@implementation NSObject (QMUITextInput_Private) + +QMUISynthesizeBOOLProperty(qmui_hasCustomizedKeyboardAppearance, setQmui_hasCustomizedKeyboardAppearance) + +static char kAssociatedObjectKey_keyboardAppearance; +- (void)setQmui_keyboardAppearance:(UIKeyboardAppearance)qmui_keyboardAppearance { + objc_setAssociatedObject(self, &kAssociatedObjectKey_keyboardAppearance, @(qmui_keyboardAppearance), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + self.qti_setKeyboardAppearanceByQMUITheme = YES; + ((UIView *)self).keyboardAppearance = qmui_keyboardAppearance; + self.qti_setKeyboardAppearanceByQMUITheme = NO; +} + +- (UIKeyboardAppearance)qmui_keyboardAppearance { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_keyboardAppearance)) integerValue]; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UITextView+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UITextView+QMUI.h index a2912121..d28423e1 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITextView+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UITextView+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITextView+QMUI.h @@ -20,6 +20,16 @@ NS_ASSUME_NONNULL_BEGIN @interface UITextView (QMUI) +/** + 立即刷新当前的 contentSize + */ +- (void)qmui_updateContentSize; + +/** + * UITextView 只有 selectedTextRange 属性(在协议里定义),这里拓展了一个方法可以将 UITextRange 类型的 selectedTextRange 转换为 NSRange 类型的 selectedRange + */ +@property(nonatomic, assign, readonly) NSRange qmui_selectedRange; + /** * convert UITextRange to NSRange, for example, [self qmui_convertNSRangeFromUITextRange:self.markedTextRange] */ diff --git a/QMUI/QMUIKit/UIKitExtensions/UITextView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UITextView+QMUI.m index 171034e3..63b4ac13 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITextView+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UITextView+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UITextView+QMUI.m @@ -15,9 +15,47 @@ #import "UITextView+QMUI.h" #import "QMUICore.h" +#import "UIScrollView+QMUI.h" @implementation UITextView (QMUI) +#ifdef IOS17_SDK_ALLOWED ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // UIScrollView.clipsToBounds 默认值为 YES,但如果是 Xcode 15 编译的包,UITextView.scrollEnabled = NO 时会强制把 clipsToBounds 置为 NO,导致 UITextView 设置了 backgroundColor 和 cornerRadius 时会看不到圆角(因为背景色溢出了),所以这里统一改回去 clipsToBounds = YES + if (@available(iOS 17.0, *)) { + OverrideImplementation([UITextView class], @selector(setScrollEnabled:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITextView *selfObject, BOOL firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if (!firstArgv) { + selfObject.clipsToBounds = YES; + } + }; + }); + } + }); +} +#endif + +- (void)qmui_updateContentSize { + SEL selector = NSSelectorFromString([NSString qmui_stringByConcat:@"_", @"updateContentSize", nil]); + if ([self respondsToSelector:selector]) { + BeginIgnorePerformSelectorLeaksWarning + [self performSelector:selector]; + EndIgnorePerformSelectorLeaksWarning + } +} + +- (NSRange)qmui_selectedRange { + return [self qmui_convertNSRangeFromUITextRange:self.selectedTextRange]; +} + - (NSRange)qmui_convertNSRangeFromUITextRange:(UITextRange *)textRange { NSInteger location = [self offsetFromPosition:self.beginningOfDocument toPosition:textRange.start]; NSInteger length = [self offsetFromPosition:textRange.start toPosition:textRange.end]; @@ -84,20 +122,22 @@ - (void)_scrollRectToVisible:(CGRect)rect animated:(BOOL)animated { CGFloat contentOffsetY = self.contentOffset.y; - if (CGRectGetMinY(rect) == self.contentOffset.y + self.textContainerInset.top) { - // 命中这个条件说明已经不用调整了,直接 return,避免继续走下面的判断,会重复调整,导致光标跳动 - return; - } - - if (CGRectGetMinY(rect) < self.contentOffset.y + self.textContainerInset.top) { - // 光标在可视区域上方,往下滚动 - contentOffsetY = CGRectGetMinY(rect) - self.textContainerInset.top - self.contentInset.top; - } else if (CGRectGetMaxY(rect) > self.contentOffset.y + CGRectGetHeight(self.bounds) - self.textContainerInset.bottom - self.contentInset.bottom) { - // 光标在可视区域下方,往上滚动 - contentOffsetY = CGRectGetMaxY(rect) - CGRectGetHeight(self.bounds) + self.textContainerInset.bottom + self.contentInset.bottom; + BOOL canScroll = self.qmui_canScroll; + if (canScroll) { + if (CGRectGetMinY(rect) < contentOffsetY + self.textContainerInset.top) { + // 光标在可视区域上方,往下滚动 + contentOffsetY = CGRectGetMinY(rect) - self.textContainerInset.top - self.adjustedContentInset.top; + } else if (CGRectGetMaxY(rect) > contentOffsetY + CGRectGetHeight(self.bounds) - self.textContainerInset.bottom - self.adjustedContentInset.bottom) { + // 光标在可视区域下方,往上滚动 + contentOffsetY = CGRectGetMaxY(rect) - CGRectGetHeight(self.bounds) + self.textContainerInset.bottom + self.adjustedContentInset.bottom; + } else { + // 光标在可视区域,不用滚动 + } + CGFloat contentOffsetWhenScrollToTop = -self.adjustedContentInset.top; + CGFloat contentOffsetWhenScrollToBottom = self.contentSize.height + self.adjustedContentInset.bottom - CGRectGetHeight(self.bounds); + contentOffsetY = MAX(MIN(contentOffsetY, contentOffsetWhenScrollToBottom), contentOffsetWhenScrollToTop); } else { - // 光标在可视区域内,不用调整 - return; + contentOffsetY = -self.adjustedContentInset.top; } [self setContentOffset:CGPointMake(self.contentOffset.x, contentOffsetY) animated:animated]; } diff --git a/QMUI/qmui/QMUIViewController.h b/QMUI/QMUIKit/UIKitExtensions/UIToolbar+QMUI.h similarity index 72% rename from QMUI/qmui/QMUIViewController.h rename to QMUI/QMUIKit/UIKitExtensions/UIToolbar+QMUI.h index 78ad8e5b..5659f140 100644 --- a/QMUI/qmui/QMUIViewController.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIToolbar+QMUI.h @@ -1,20 +1,23 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ - + */ // -// QMUIViewController.h -// qmui +// UIToolbar+QMUI.h +// QMUIKit // -// Created by QMUI Team on 15/4/13. +// Created by MoLice on 2021/N/24. // -#import "QMUICommonViewController.h" +#import + +NS_ASSUME_NONNULL_BEGIN -@interface QMUIViewController : QMUICommonViewController +@interface UIToolbar (QMUI) @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIToolbar+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIToolbar+QMUI.m new file mode 100644 index 00000000..7a6fd0fd --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIToolbar+QMUI.m @@ -0,0 +1,134 @@ +/** + * 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. + */ +// +// UIToolbar+QMUI.m +// QMUIKit +// +// Created by MoLice on 2021/N/24. +// + +#import "UIToolbar+QMUI.h" +#import "QMUICore.h" + +@implementation UIToolbar (QMUI) + +#ifdef IOS15_SDK_ALLOWED ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // 以下是将 iOS 12 修改 UIToolbar 样式的接口转换成用 iOS 13 的新接口去设置(因为新旧方法是互斥的,所以统一在新系统都用新方法) + // 虽然系统的新接口是 iOS 13 就已经存在,但由于 iOS 13、14 都没必要用新接口,所以 QMUI 里在 iOS 15 才开始使用新接口,所以下方的 @available 填的是 iOS 15 而非 iOS 13(与 QMUIConfiguration.m 对应)。 + // 但这样有个风险,因为 QMUIConfiguration 配置表里都是用 appearance 的方式去设置 standardAppearance,所以如果在 UIToolbar 实例被添加到 window 之前修改过旧版任意一个样式接口,就会导致一个新的 UIToolbarAppearance 对象被设置给 standardAppearance 属性,这样系统就会认为你这个 UIToolbar 实例自定义了 standardAppearance,那么当它被 moveToWindow 时就不会自动应用 appearance 的值了,因此需要保证在添加到 window 前不要自行修改属性 + if (@available(iOS 15.0, *)) { + + void (^syncAppearance)(UIToolbar *, void(^barActionBlock)(UIToolbarAppearance *appearance)) = ^void(UIToolbar *toolbar, void(^barActionBlock)(UIToolbarAppearance *appearance)) { + if (!barActionBlock) return; + + UIToolbarAppearance *appearance = toolbar.standardAppearance; + barActionBlock(appearance); + toolbar.standardAppearance = appearance; + if (QMUICMIActivated && ToolBarUsesStandardAppearanceOnly) { + toolbar.scrollEdgeAppearance = appearance; + } + }; + + OverrideImplementation([UIToolbar class], @selector(setBarTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIToolbar *selfObject, UIColor *barTintColor) { + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, barTintColor); + + syncAppearance(selfObject, ^void(UIToolbarAppearance *appearance) { + appearance.backgroundColor = barTintColor; + }); + }; + }); + + OverrideImplementation([UIToolbar class], @selector(barTintColor), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIColor *(UIToolbar *selfObject) { + return selfObject.standardAppearance.backgroundColor; + }; + }); + + OverrideImplementation([UIToolbar class], @selector(setBackgroundImage:forToolbarPosition:barMetrics:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIToolbar *selfObject, UIImage *image, UIBarPosition barPosition, UIBarMetrics barMetrics) { + + // call super + void (*originSelectorIMP)(id, SEL, UIImage *, UIBarPosition, UIBarMetrics); + originSelectorIMP = (void (*)(id, SEL, UIImage *, UIBarPosition, UIBarMetrics))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, image, barPosition, barMetrics); + + syncAppearance(selfObject, ^void(UIToolbarAppearance *appearance) { + appearance.backgroundImage = image; + }); + }; + }); + + OverrideImplementation([UIToolbar class], @selector(backgroundImageForToolbarPosition:barMetrics:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UIToolbar *selfObject, UIBarPosition firstArgv, UIBarMetrics secondArgv) { + return selfObject.standardAppearance.backgroundImage; + }; + }); + + OverrideImplementation([UIToolbar class], @selector(setShadowImage:forToolbarPosition:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIToolbar *selfObject, UIImage *shadowImage, UIBarPosition position) { + + // call super + void (*originSelectorIMP)(id, SEL, UIImage *, UIBarPosition); + originSelectorIMP = (void (*)(id, SEL, UIImage *, UIBarPosition))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, shadowImage, position); + + syncAppearance(selfObject, ^void(UIToolbarAppearance *appearance) { + appearance.shadowImage = shadowImage; + }); + }; + }); + + OverrideImplementation([UIToolbar class], @selector(shadowImageForToolbarPosition:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UIToolbar *selfObject, UIBarPosition position) { + return selfObject.standardAppearance.shadowImage; + }; + }); + +// OverrideImplementation([UIToolbar class], @selector(setBarStyle:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { +// return ^(UIToolbar *selfObject, UIBarStyle barStyle) { +// +// // call super +// void (*originSelectorIMP)(id, SEL, UIBarStyle); +// originSelectorIMP = (void (*)(id, SEL, UIBarStyle))originalIMPProvider(); +// originSelectorIMP(selfObject, originCMD, barStyle); +// +// syncAppearance(selfObject, ^void(UIToolbarAppearance *appearance) { +// appearance.backgroundEffect = [UIBlurEffect effectWithStyle:barStyle == UIBarStyleDefault ? UIBlurEffectStyleSystemChromeMaterialLight : UIBlurEffectStyleSystemChromeMaterialDark]; +// }); +// }; +// }); + + // iOS 15 没有对应的属性 +// OverrideImplementation([UIToolbar class], @selector(barStyle), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { +// return ^UIBarStyle(UIToolbar *selfObject) { +// +// if (@available(iOS 15.0, *)) { +// return ???; +// } +// +// // call super +// UIBarStyle (*originSelectorIMP)(id, SEL); +// originSelectorIMP = (UIBarStyle (*)(id, SEL))originalIMPProvider(); +// UIBarStyle result = originSelectorIMP(selfObject, originCMD); +// +// return result; +// }; +// }); + } + }); +} +#endif +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UITraitCollection+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UITraitCollection+QMUI.h new file mode 100644 index 00000000..b24c2a0a --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UITraitCollection+QMUI.h @@ -0,0 +1,33 @@ +/** + * 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. + */ +// +// UITraitCollection+QMUI.h +// QMUIKit +// +// Created by ziezheng on 2019/7/19. +// + + +#import + +NS_ASSUME_NONNULL_BEGIN + + +@interface UITraitCollection (QMUI) + +/** + 添加一个系统的深色、浅色外观发即将生变化前的监听,可用于需要在外观即将发生改变之前更新状态,例如 QMUIThemeManager 利用其来自动切换主题 + @note 如果在 info.plist 中指定 User Interface Style 值将无法监听。 + */ ++ (void)qmui_addUserInterfaceStyleWillChangeObserver:(id)observer selector:(SEL)aSelector API_AVAILABLE(ios(13.0)); + +@end + + + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UITraitCollection+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UITraitCollection+QMUI.m new file mode 100644 index 00000000..843ea3de --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UITraitCollection+QMUI.m @@ -0,0 +1,118 @@ +/** + * 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. + */ +// +// UITraitCollection+QMUI.m +// QMUIKit +// +// Created by ziezheng on 2019/7/19. +// + +#import "UITraitCollection+QMUI.h" +#import "QMUICore.h" + +@implementation UITraitCollection (QMUI) + +static NSHashTable *_eventObservers; +static NSString * const kQMUIUserInterfaceStyleWillChangeSelectorsKey = @"qmui_userInterfaceStyleWillChangeObserver"; + ++ (void)qmui_addUserInterfaceStyleWillChangeObserver:(id)observer selector:(SEL)aSelector { + @synchronized (self) { + [UITraitCollection _qmui_overrideTraitCollectionMethodIfNeeded]; + if (!_eventObservers) { + _eventObservers = [NSHashTable weakObjectsHashTable]; + } + NSMutableSet *selectors = [observer qmui_getBoundObjectForKey:kQMUIUserInterfaceStyleWillChangeSelectorsKey]; + if (!selectors) { + selectors = [NSMutableSet set]; + [observer qmui_bindObject:selectors forKey:kQMUIUserInterfaceStyleWillChangeSelectorsKey]; + } + [selectors addObject:NSStringFromSelector(aSelector)]; + [_eventObservers addObject:observer]; + } +} + ++ (void)_qmui_notifyUserInterfaceStyleWillChangeEvents:(UITraitCollection *)traitCollection { + NSHashTable *eventObservers = [_eventObservers copy]; + for (id observer in eventObservers) { + NSMutableSet *selectors = [observer qmui_getBoundObjectForKey:kQMUIUserInterfaceStyleWillChangeSelectorsKey]; + for (NSString *selectorString in selectors) { + SEL selector = NSSelectorFromString(selectorString); + if ([observer respondsToSelector:selector]) { + NSMethodSignature *methodSignature = [observer methodSignatureForSelector:selector]; + NSUInteger numberOfArguments = [methodSignature numberOfArguments] - 2; // 减去 self cmd 隐形参数剩下的参数数量 + QMUIAssert(numberOfArguments <= 1, @"UITraitCollection (QMUI)", @"observer 的 selector 参数超过 1 个"); + BeginIgnorePerformSelectorLeaksWarning + if (numberOfArguments == 0) { + [observer performSelector:selector]; + } else if (numberOfArguments == 1) { + [observer performSelector:selector withObject:traitCollection]; + } + EndIgnorePerformSelectorLeaksWarning + } + } + } +} + ++ (void)_qmui_overrideTraitCollectionMethodIfNeeded { + [QMUIHelper executeBlock:^{ + static UIUserInterfaceStyle qmui_lastNotifiedUserInterfaceStyle; + qmui_lastNotifiedUserInterfaceStyle = [UITraitCollection currentTraitCollection].userInterfaceStyle; + + // - (void) _willTransitionToTraitCollection:(id)arg1 withTransitionCoordinator:(id)arg2; (0x7fff24711d49) + OverrideImplementation([UIWindow class], NSSelectorFromString([NSString qmui_stringByConcat:@"_", @"willTransitionToTraitCollection:", @"withTransitionCoordinator:", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIWindow *selfObject, UITraitCollection *traitCollection, id coordinator) { + + // call super + void (*originSelectorIMP)(id, SEL, UITraitCollection *, id ); + originSelectorIMP = (void (*)(id, SEL, UITraitCollection *, id ))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, traitCollection, coordinator); + + BOOL snapshotFinishedOnBackground = traitCollection.userInterfaceLevel == UIUserInterfaceLevelElevated && UIApplication.sharedApplication.applicationState == UIApplicationStateBackground; + // 进入后台且完成截图了就不继续去响应 style 变化(实测 iOS 13.0 iPad 进入后台并完成截图后,仍会多次改变 style,但是系统并没有调用界面的相关刷新方法) + if (selfObject.windowScene && !snapshotFinishedOnBackground) { + UIWindow *firstValidatedWindow = nil; + + if ([NSStringFromClass(selfObject.class) containsString:@"_UIWindowSceneUserInterfaceStyle"]) { // _UIWindowSceneUserInterfaceStyleAnimationSnapshotWindow + firstValidatedWindow = selfObject; + } else { + // 系统会按照这个数组的顺序去更新 window 的 traitCollection,找出最先响应样式更新的 window + NSPointerArray *windows = [[selfObject windowScene] valueForKeyPath:@"_contextBinder._attachedBindables"]; + for (NSUInteger i = 0, count = windows.count; i < count; i++) { + UIWindow *window = [windows pointerAtIndex:i]; + // 例如用 UIWindow 方式显示的弹窗,在消失后,在 windows 数组里会残留一个 nil 的位置,这里过滤掉,否则会导致 App 从桌面唤醒时无法立即显示正确的 style + if (!window) { + continue;; + } + + // 由于 Keyboard 可以通过 keyboardAppearance 来控制 userInterfaceStyle 的 Dark/Light,不一定和系统一样,这里要过滤掉 + if ([window isKindOfClass:NSClassFromString(@"UIRemoteKeyboardWindow")] || [window isKindOfClass:NSClassFromString(@"UITextEffectsWindow")]) { + continue; + } + if (window.overrideUserInterfaceStyle != UIUserInterfaceStyleUnspecified) { + // 这里需要获取到和系统样式同步的 UserInterfaceStyle(所以指定 overrideUserInterfaceStyle 需要跳过) + // 所以当全部 window.overrideUserInterfaceStyle 都指定为非 UIUserInterfaceStyleUnspecified 时将无法获得当前系统的外观 + continue; + } + firstValidatedWindow = window; + break; + } + } + + if (selfObject == firstValidatedWindow) { + if (qmui_lastNotifiedUserInterfaceStyle != traitCollection.userInterfaceStyle) { + qmui_lastNotifiedUserInterfaceStyle = traitCollection.userInterfaceStyle; + [self _qmui_notifyUserInterfaceStyleWillChangeEvents:traitCollection]; + } + } + } + }; + }); + } oncePerIdentifier:@"UITraitCollection addUserInterfaceStyleWillChangeObserver"]; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIView+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIView+QMUI.h index 89160937..e72e3f7d 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIView+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIView+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIView+QMUI.h @@ -15,6 +15,7 @@ #import #import +#import "UIView+QMUIBorder.h" NS_ASSUME_NONNULL_BEGIN @@ -30,13 +31,24 @@ NS_ASSUME_NONNULL_BEGIN /** 将要设置的 frame 用 CGRectApplyAffineTransformWithAnchorPoint 处理后再设置 + 注意这个方式会导致 self.bounds 也受 transform 的影响(系统默认行为是 frame 受 transform 影响,center 和 bounds 不会),如果有需要访问 self.bounds 的情况,请避免使用这个方式。 */ @property(nonatomic, assign) CGRect qmui_frameApplyTransform; /** 在 iOS 11 及之后的版本,此属性将返回系统已有的 self.safeAreaInsets。在之前的版本此属性返回 UIEdgeInsetsZero */ -@property(nonatomic, assign, readonly) UIEdgeInsets qmui_safeAreaInsets; +@property(nonatomic, assign, readonly) UIEdgeInsets qmui_safeAreaInsets DEPRECATED_MSG_ATTRIBUTE("请使用系统的 UIView.safeAreaInsets,QMUI 4.4.0 已不再支持 iOS 10,没必要提供该兼容性之的接口了,后续会删除。"); + +/** + 有修改过 tintColor,则不会再受 superview.tintColor 的影响 + */ +@property(nonatomic, assign, readonly) BOOL qmui_tintColorCustomized; + +/// 响应区域需要改变的大小,负值表示往外扩大,正值表示往内缩小。 +/// 特别地,如果对 UISlider 使用,则扩大的是圆点的区域。 +/// 当你引入了 QMUINavigationButton,它会使 UIBarButtonItem.customView 也可使用 qmui_outsideEdge(默认不可以,因为 customView 的父容器和 customView 一样大,所以 UINavigationBar 感知不到 customView 有 qmui_outsideEdge)。 +@property(nonatomic,assign) UIEdgeInsets qmui_outsideEdge; /** 移除当前所有 subviews @@ -81,12 +93,19 @@ NS_ASSUME_NONNULL_BEGIN @property(nullable, nonatomic, copy) void (^qmui_frameDidChangeBlock)(__kindof UIView *view, CGRect precedingFrame); /** - 在 UIView 的 layoutSubviews 调用后的下一个 runloop 调用。如果不放到下一个 runloop,直接就调用,会导致先于子类重写的 layoutSubviews 就调用,这样就无法获取到正确的 subviews 的布局。 + 在 - [UIView layoutSubviews] 调用后就调用的 block @param view 当前的 view 本身,方便使用,省去 weak 操作 - @note 如果某些 view 重写了 layoutSubviews 但没有调用 super,则这个 block 也不会被调用 */ @property(nullable, nonatomic, copy) void (^qmui_layoutSubviewsBlock)(__kindof UIView *view); +/** + 在 UIView 的 sizeThatFits: 调用后就调用的 block,可返回一个修改后的值来作为原方法的返回值 + @param view 当前的 view 本身,方便使用,省去 weak 操作 + @param size sizeThatFits: 方法被调用时传进来的参数 size + @param superResult 原本的 sizeThatFits: 方法的返回值 + */ +@property(nullable, nonatomic, copy) CGSize (^qmui_sizeThatFitsBlock)(__kindof UIView *view, CGSize size, CGSize superResult); + /** 当 tintColorDidChange 被调用的时候会调用这个 block,就不用重写方法了 @param view 当前的 view 本身,方便使用,省去 weak 操作 @@ -97,9 +116,9 @@ NS_ASSUME_NONNULL_BEGIN 当 hitTest:withEvent: 被调用时会调用这个 block,就不用重写方法了 @param point 事件产生的 point @param event 事件 - @param super 的返回结果 + @param originalView super 的返回结果 */ -@property(nullable, nonatomic, copy) __kindof UIView * (^qmui_hitTestBlock)(CGPoint point, UIEvent *event, __kindof UIView *originalView); +@property(nullable, nonatomic, copy) __kindof UIView * _Nullable (^qmui_hitTestBlock)(CGPoint point, UIEvent * _Nullable event, __kindof UIView * _Nullable originalView); @end @@ -134,51 +153,6 @@ NS_ASSUME_NONNULL_BEGIN @end -typedef NS_OPTIONS(NSUInteger, QMUIViewBorderPosition) { - QMUIViewBorderPositionNone = 0, - QMUIViewBorderPositionTop = 1 << 0, - QMUIViewBorderPositionLeft = 1 << 1, - QMUIViewBorderPositionBottom = 1 << 2, - QMUIViewBorderPositionRight = 1 << 3 -}; - -typedef NS_ENUM(NSUInteger, QMUIViewBorderLocation) { - QMUIViewBorderLocationInside, - QMUIViewBorderLocationCenter, - QMUIViewBorderLocationOutside -}; - -/** - * UIView (QMUI_Border) 为 UIView 方便地显示某几个方向上的边框。 - * - * 系统的默认实现里,要为 UIView 加边框一般是通过 view.layer 来实现,view.layer 会给四条边都加上边框,如果你只想为其中某几条加上边框就很麻烦,于是 UIView (QMUI_Border) 提供了 qmui_borderPosition 来解决这个问题。 - * @warning 注意如果你需要为 UIView 四条边都加上边框,请使用系统默认的 view.layer 来实现,而不要用 UIView (QMUI_Border),会浪费资源,这也是为什么 QMUIViewBorderPosition 不提供一个 QMUIViewBorderPositionAll 枚举值的原因。 - */ -@interface UIView (QMUI_Border) - -/// 设置边框的位置,默认为 QMUIViewBorderLocationInside,与 view.layer.border 一致。 -@property(nonatomic, assign) QMUIViewBorderLocation qmui_borderLocation; - -/// 设置边框类型,支持组合,例如:`borderPosition = QMUIViewBorderPositionTop|QMUIViewBorderPositionBottom`。默认为 QMUIViewBorderPositionNone。 -@property(nonatomic, assign) QMUIViewBorderPosition qmui_borderPosition; - -/// 边框的大小,默认为PixelOne。请注意修改 qmui_borderPosition 的值以将边框显示出来。 -@property(nonatomic, assign) IBInspectable CGFloat qmui_borderWidth; - -/// 边框的颜色,默认为UIColorSeparator。请注意修改 qmui_borderPosition 的值以将边框显示出来。 -@property(nullable, nonatomic, strong) IBInspectable UIColor *qmui_borderColor; - -/// 虚线 : dashPhase默认是0,且当dashPattern设置了才有效 -/// qmui_dashPhase 表示虚线起始的偏移,qmui_dashPattern 可以传一个数组,表示“lineWidth,lineSpacing,lineWidth,lineSpacing...”的顺序,至少传 2 个。 -@property(nonatomic, assign) CGFloat qmui_dashPhase; -@property(nullable, nonatomic, copy) NSArray *qmui_dashPattern; - -/// border的layer -@property(nullable, nonatomic, strong, readonly) CAShapeLayer *qmui_borderLayer; - -@end - - /** * 方便地将某个 UIView 截图并转成一个 UIImage,注意如果这个 UIView 本身做了 transform,也不会在截图上反映出来,截图始终都是原始 UIView 的截图。 */ @@ -220,12 +194,26 @@ extern const CGFloat QMUIViewSelfSizingHeight; /// 等价于 CGRectGetMaxX(frame) @property(nonatomic, assign) CGFloat qmui_right; +/// 以 center = xxx 的方式将 frame 的 origin 设置为指定的值,由于用的是 center,所以可以兼容 transform 场景。 +@property(nonatomic, assign) CGPoint qmui_origin; + /// 等价于 CGRectGetWidth(frame) @property(nonatomic, assign) CGFloat qmui_width; /// 等价于 CGRectGetHeight(frame) @property(nonatomic, assign) CGFloat qmui_height; +/// 等价于 self.frame.size +@property(nonatomic, assign) CGSize qmui_size; + +extern const CGSize QMUIViewFixedSizeNone; + +/// 把当前 view 的大小设置为某个值并且固定下来(保证 setFrame:、setBounds: 等操作也无法影响它的 size),sizeThatFits: 返回的结果也以这个为准(但如果业务重写了就以业务的为准) +/// 默认为 QMUIViewFixedSizeNone,也即不处理(如果你设置过 fixedSize,后续又希望去掉这个特性,也可把 fixedSize 赋值为 QMUIViewFixedSizeNone 来清空)。 +/// @example 例如 UIButton 的 imageView 是无法固定大小的,但如果你要把一张网络上下载的图(大小 不确定)作为 image 放到 button 里,就可以用 qmui_fixedSize 将 imageView 限制为某个尺寸,从而兼容不同的网络图片。 +/// @warning 内部使用 qmui_sizeThatFitsBlock 实现(因为某些系统的 View 重写了 UIView 的 sizeThatFits,为了保证 qmui_fixedSize 生效,只能用 qmui_sizeThatFitsBlock),所以不要同时使用两者。 +@property(nonatomic, assign) CGSize qmui_fixedSize; + /// 保持其他三个边缘的位置不变的情况下,将顶边缘拓展到某个指定的位置,注意高度会跟随变化。 @property(nonatomic, assign) CGFloat qmui_extendToTop; @@ -272,8 +260,6 @@ extern const CGFloat QMUIViewSelfSizingHeight; @property(nonatomic, assign) BOOL qmui_shouldShowDebugColor; /// 是否每个view的背景色随机,如果不随机则统一使用半透明红色,默认NO @property(nonatomic, assign) BOOL qmui_needsDifferentDebugColor; -/// 标记一个view是否已经被添加了debug背景色,外部一般不使用 -@property(nonatomic, assign, readonly) BOOL qmui_hasDebugColor; @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIView+QMUI.m index 0f2ab947..faf17139 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIView+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIView+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIView+QMUI.m @@ -15,7 +15,6 @@ #import "UIView+QMUI.h" #import "QMUICore.h" -#import "CALayer+QMUI.h" #import "UIColor+QMUI.h" #import "NSObject+QMUI.h" #import "UIImage+QMUI.h" @@ -23,219 +22,40 @@ #import "UIViewController+QMUI.h" #import "QMUILog.h" #import "QMUIWeakObjectContainer.h" -#import - -@interface UIView () - -/// QMUI_Debug -@property(nonatomic, assign, readwrite) BOOL qmui_hasDebugColor; - -/// QMUI_Border -@property(nonatomic, strong, readwrite) CAShapeLayer *qmui_borderLayer; - -@end - @implementation UIView (QMUI) +QMUISynthesizeBOOLProperty(qmui_tintColorCustomized, setQmui_tintColorCustomized) QMUISynthesizeIdCopyProperty(qmui_frameWillChangeBlock, setQmui_frameWillChangeBlock) QMUISynthesizeIdCopyProperty(qmui_frameDidChangeBlock, setQmui_frameDidChangeBlock) -QMUISynthesizeIdCopyProperty(qmui_layoutSubviewsBlock, setQmui_layoutSubviewsBlock) -QMUISynthesizeIdCopyProperty(qmui_tintColorDidChangeBlock, setQmui_tintColorDidChangeBlock) -QMUISynthesizeIdCopyProperty(qmui_hitTestBlock, setQmui_hitTestBlock) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ExtendImplementationOfVoidMethodWithoutArguments([UIView class], @selector(tintColorDidChange), ^(UIView *selfObject) { - if (selfObject.qmui_tintColorDidChangeBlock) { - selfObject.qmui_tintColorDidChangeBlock(selfObject); - } - }); - - ExtendImplementationOfNonVoidMethodWithTwoArguments([UIView class], @selector(hitTest:withEvent:), CGPoint, UIEvent *, UIView *, ^UIView *(UIView *selfObject, CGPoint point, UIEvent *event, UIView *originReturnValue) { - if (selfObject.qmui_hitTestBlock) { - UIView *view = selfObject.qmui_hitTestBlock(point, event, originReturnValue); - return view; - } - return originReturnValue; - }); - - OverrideImplementation([UIView class], @selector(becomeFirstResponder), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^BOOL(UIView *selfObject) { - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (IS_SIMULATOR && ![selfObject isKindOfClass:[UIWindow class]] && (selfObject.window ? !selfObject.window.keyWindow : YES) && !selfObject.qmui_visible) { - [selfObject QMUISymbolicUIViewBecomeFirstResponderWithoutKeyWindow]; - } - } - - // call super - BOOL (*originSelectorIMP)(id, SEL); - originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider(); - BOOL result = originSelectorIMP(selfObject, originCMD); - - return result; - }; - }); - - OverrideImplementation([UIView class], @selector(addSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIView *selfObject, UIView *view) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (view == selfObject) { - [selfObject printLogForAddSubviewToSelf]; - return; - } - } + OverrideImplementation([UIView class], @selector(setTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, UIColor *tintColor) { // call super - void (*originSelectorIMP)(id, SEL, UIView *); - originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, view); - }; - }); - - OverrideImplementation([UIView class], @selector(insertSubview:atIndex:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIView *selfObject, UIView *view, NSInteger index) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (view == selfObject) { - [selfObject printLogForAddSubviewToSelf]; - return; - } - } - - // call super - void (*originSelectorIMP)(id, SEL, UIView *, NSInteger); - originSelectorIMP = (void (*)(id, SEL, UIView *, NSInteger))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, view, index); - }; - }); - - OverrideImplementation([UIView class], @selector(insertSubview:aboveSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIView *selfObject, UIView *view, UIView *siblingSubview) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (view == self) { - [selfObject printLogForAddSubviewToSelf]; - return; - } - } + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, tintColor); - // call super - void (*originSelectorIMP)(id, SEL, UIView *, UIView *); - originSelectorIMP = (void (*)(id, SEL, UIView *, UIView *))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, view, siblingSubview); + selfObject.qmui_tintColorCustomized = !!tintColor; }; }); - OverrideImplementation([UIView class], @selector(insertSubview:belowSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIView *selfObject, UIView *view, UIView *siblingSubview) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (view == self) { - [selfObject printLogForAddSubviewToSelf]; - return; + // 这个私有方法在 view 被调用 becomeFirstResponder 并且处于 window 上时,才会被调用,所以比 becomeFirstResponder 更适合用来检测 + ExtendImplementationOfVoidMethodWithSingleArgument([UIView class], NSSelectorFromString(@"_didChangeToFirstResponder:"), id, ^(UIView *selfObject, id firstArgv) { + if (selfObject == firstArgv && [selfObject conformsToProtocol:@protocol(UITextInput)]) { + // 像 QMUIModalPresentationViewController 那种以 window 的形式展示浮层,浮层里的输入框 becomeFirstResponder 的场景,[window makeKeyAndVisible] 被调用后,就会立即走到这里,但此时该 window 尚不是 keyWindow,所以这里延迟到下一个 runloop 里再去判断 + dispatch_async(dispatch_get_main_queue(), ^{ + if (IS_DEBUG && ![selfObject isKindOfClass:[UIWindow class]] && selfObject.window && !selfObject.window.keyWindow) { + [selfObject QMUISymbolicUIViewBecomeFirstResponderWithoutKeyWindow]; } - } - - // call super - void (*originSelectorIMP)(id, SEL, UIView *, UIView *); - originSelectorIMP = (void (*)(id, SEL, UIView *, UIView *))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, view, siblingSubview); - }; - }); - - OverrideImplementation([UIView class], @selector(convertPoint:toView:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^CGPoint(UIView *selfObject, CGPoint point, UIView *view) { - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - [selfObject alertConvertValueWithView:view]; - } - - // call super - CGPoint (*originSelectorIMP)(id, SEL, CGPoint, UIView *); - originSelectorIMP = (CGPoint (*)(id, SEL, CGPoint, UIView *))originalIMPProvider(); - CGPoint result = originSelectorIMP(selfObject, originCMD, point, view); - - return result; - }; - }); - - OverrideImplementation([UIView class], @selector(convertPoint:fromView:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^CGPoint(UIView *selfObject, CGPoint point, UIView *view) { - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - [selfObject alertConvertValueWithView:view]; - } - - // call super - CGPoint (*originSelectorIMP)(id, SEL, CGPoint, UIView *); - originSelectorIMP = (CGPoint (*)(id, SEL, CGPoint, UIView *))originalIMPProvider(); - CGPoint result = originSelectorIMP(selfObject, originCMD, point, view); - - return result; - }; - }); - - OverrideImplementation([UIView class], @selector(convertRect:toView:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^CGRect(UIView *selfObject, CGRect rect, UIView *view) { - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - [selfObject alertConvertValueWithView:view]; - } - - // call super - CGRect (*originSelectorIMP)(id, SEL, CGRect, UIView *); - originSelectorIMP = (CGRect (*)(id, SEL, CGRect, UIView *))originalIMPProvider(); - CGRect result = originSelectorIMP(selfObject, originCMD, rect, view); - - return result; - }; - }); - - OverrideImplementation([UIView class], @selector(convertRect:fromView:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^CGRect(UIView *selfObject, CGRect rect, UIView *view) { - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - [selfObject alertConvertValueWithView:view]; - } - - // call super - CGRect (*originSelectorIMP)(id, SEL, CGRect, UIView *); - originSelectorIMP = (CGRect (*)(id, SEL, CGRect, UIView *))originalIMPProvider(); - CGRect result = originSelectorIMP(selfObject, originCMD, rect, view); - - return result; - }; + }); + } }); - - - - // 目前发现 UIButton、UITabBarButton 等系统的 class 的 layoutSubviews 内没有调用 super,导致 UIView (QMUI) 里的重写不生效,所以要专门为这些 class 每个都重写一次 - NSMutableArray *classes = @[UIView.class, UIButton.class].mutableCopy; - if (IOS_VERSION_NUMBER < 93000) { - [classes addObject:NSClassFromString([NSString stringWithFormat:@"%@%@", @"UITab", @"BarButton"])]; - } - [classes enumerateObjectsUsingBlock:^(Class obj, NSUInteger idx, BOOL * _Nonnull stop) { - ExtendImplementationOfVoidMethodWithoutArguments(obj, @selector(layoutSubviews), ^(__kindof UIView *selfObject) { - // 放到下一个 runloop 是为了保证比子类的 layoutSubviews 逻辑要更晚调用 - if (selfObject.qmui_layoutSubviewsBlock) { - dispatch_async(dispatch_get_main_queue(), ^{ - if (selfObject.qmui_layoutSubviewsBlock) { - selfObject.qmui_layoutSubviewsBlock(selfObject); - } - }); - } - }); - }]; }); } @@ -252,16 +72,119 @@ - (CGRect)qmui_frameApplyTransform { } - (UIEdgeInsets)qmui_safeAreaInsets { - if (@available(iOS 11.0, *)) { - return self.safeAreaInsets; - } - return UIEdgeInsetsZero; + return self.safeAreaInsets; } - (void)qmui_removeAllSubviews { [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; } +static char kAssociatedObjectKey_outsideEdge; +- (void)setQmui_outsideEdge:(UIEdgeInsets)qmui_outsideEdge { + objc_setAssociatedObject(self, &kAssociatedObjectKey_outsideEdge, @(qmui_outsideEdge), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + if (!UIEdgeInsetsEqualToEdgeInsets(qmui_outsideEdge, UIEdgeInsetsZero)) { + [QMUIHelper executeBlock:^{ + OverrideImplementation([UIView class], @selector(pointInside:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^BOOL(UIControl *selfObject, CGPoint point, UIEvent *event) { + + if (!UIEdgeInsetsEqualToEdgeInsets(selfObject.qmui_outsideEdge, UIEdgeInsetsZero) + && selfObject.alpha > 0.01 + && !selfObject.hidden + && !CGRectIsEmpty(selfObject.frame)) { + CGRect rect = UIEdgeInsetsInsetRect(selfObject.bounds, selfObject.qmui_outsideEdge); + BOOL result = CGRectContainsPoint(rect, point); + return result; + } + + // call super + BOOL (*originSelectorIMP)(id, SEL, CGPoint, UIEvent *); + originSelectorIMP = (BOOL (*)(id, SEL, CGPoint, UIEvent *))originalIMPProvider(); + BOOL result = originSelectorIMP(selfObject, originCMD, point, event); + return result; + }; + }); + } oncePerIdentifier:@"UIView (QMUI) outsideEdge"]; + + if ([self isKindOfClass:UISlider.class]) { + [QMUIHelper executeBlock:^{ + if (@available(iOS 14.0, *)) { + // -[_UISlideriOSVisualElement thumbHitEdgeInsets] + OverrideImplementation(NSClassFromString([NSString qmui_stringByConcat:@"_", @"UISlider", @"iOS", @"VisualElement", nil]), NSSelectorFromString(@"thumbHitEdgeInsets"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIEdgeInsets(UIView *selfObject) { + // call super + UIEdgeInsets (*originSelectorIMP)(id, SEL); + originSelectorIMP = (UIEdgeInsets (*)(id, SEL))originalIMPProvider(); + UIEdgeInsets result = originSelectorIMP(selfObject, originCMD); + + UISlider *slider = (UISlider *)selfObject.superview; + if ([slider isKindOfClass:UISlider.class] && !UIEdgeInsetsEqualToEdgeInsets(slider.qmui_outsideEdge, UIEdgeInsetsZero)) { + result = UIEdgeInsetsConcat(result, slider.qmui_outsideEdge); + } + return result; + }; + }); + } else { + // -[UISlider _thumbHitEdgeInsets] + OverrideImplementation([UISlider class], NSSelectorFromString(@"_thumbHitEdgeInsets"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIEdgeInsets(UISlider *selfObject) { + // call super + UIEdgeInsets (*originSelectorIMP)(id, SEL); + originSelectorIMP = (UIEdgeInsets (*)(id, SEL))originalIMPProvider(); + UIEdgeInsets result = originSelectorIMP(selfObject, originCMD); + + if (!UIEdgeInsetsEqualToEdgeInsets(selfObject.qmui_outsideEdge, UIEdgeInsetsZero)) { + result = UIEdgeInsetsConcat(result, selfObject.qmui_outsideEdge); + } + return result; + }; + }); + } + } oncePerIdentifier:@"UIView (QMUI) outsideEdge slider"]; + } + } +} + +- (UIEdgeInsets)qmui_outsideEdge { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_outsideEdge)) UIEdgeInsetsValue]; +} + +static char kAssociatedObjectKey_tintColorDidChangeBlock; +- (void)setQmui_tintColorDidChangeBlock:(void (^)(__kindof UIView * _Nonnull))qmui_tintColorDidChangeBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_tintColorDidChangeBlock, qmui_tintColorDidChangeBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_tintColorDidChangeBlock) { + [QMUIHelper executeBlock:^{ + ExtendImplementationOfVoidMethodWithoutArguments([UIView class], @selector(tintColorDidChange), ^(UIView *selfObject) { + if (selfObject.qmui_tintColorDidChangeBlock) { + selfObject.qmui_tintColorDidChangeBlock(selfObject); + } + }); + } oncePerIdentifier:@"UIView (QMUI) tintColorDidChangeBlock"]; + } +} + +- (void (^)(__kindof UIView * _Nonnull))qmui_tintColorDidChangeBlock { + return (void (^)(__kindof UIView * _Nonnull))objc_getAssociatedObject(self, &kAssociatedObjectKey_tintColorDidChangeBlock); +} + +static char kAssociatedObjectKey_hitTestBlock; +- (void)setQmui_hitTestBlock:(__kindof UIView * _Nullable (^)(CGPoint, UIEvent * _Nullable, __kindof UIView * _Nullable))qmui_hitTestBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_hitTestBlock, qmui_hitTestBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + [QMUIHelper executeBlock:^{ + ExtendImplementationOfNonVoidMethodWithTwoArguments([UIView class], @selector(hitTest:withEvent:), CGPoint, UIEvent *, UIView *, ^UIView *(UIView *selfObject, CGPoint point, UIEvent *event, UIView *originReturnValue) { + if (selfObject.qmui_hitTestBlock) { + UIView *view = selfObject.qmui_hitTestBlock(point, event, originReturnValue); + return view; + } + return originReturnValue; + }); + } oncePerIdentifier:@"UIView (QMUI) hitTestBlock"]; +} + +- (__kindof UIView * _Nonnull (^)(CGPoint, UIEvent * _Nonnull, __kindof UIView * _Nonnull))qmui_hitTestBlock { + return (__kindof UIView * _Nonnull (^)(CGPoint, UIEvent * _Nonnull, __kindof UIView * _Nonnull))objc_getAssociatedObject(self, &kAssociatedObjectKey_hitTestBlock); +} + - (CGPoint)qmui_convertPoint:(CGPoint)point toView:(nullable UIView *)view { if (view) { return [view qmui_convertPoint:point fromView:view]; @@ -349,49 +272,10 @@ + (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duratio } } -- (void)printLogForAddSubviewToSelf { - UIViewController *visibleViewController = [QMUIHelper visibleViewController]; - NSString *log = [NSString stringWithFormat:@"UIView (QMUI) addSubview:, 把自己作为 subview 添加到自己身上,self = %@, visibleViewController = %@, visibleState = %@, viewControllers = %@\n%@", self, visibleViewController, @(visibleViewController.qmui_visibleState), visibleViewController.navigationController.viewControllers, [NSThread callStackSymbols]]; - NSAssert(NO, log); - QMUILogWarn(@"UIView (QMUI)", @"%@", log); -} - - (void)QMUISymbolicUIViewBecomeFirstResponderWithoutKeyWindow { QMUILogWarn(@"UIView (QMUI)", @"尝试让一个处于非 keyWindow 上的 %@ becomeFirstResponder,可能导致界面显示异常,请添加 '%@' 的 Symbolic Breakpoint 以捕捉此类信息\n%@", NSStringFromClass(self.class), NSStringFromSelector(_cmd), [NSThread callStackSymbols]); } -- (BOOL)hasSharedAncestorViewWithView:(UIView *)view { - UIView *sharedAncestorView = self; - if (!view) { - return YES; - } - while (sharedAncestorView && ![view isDescendantOfView:sharedAncestorView]) { - sharedAncestorView = sharedAncestorView.superview; - } - return !!sharedAncestorView; -} - -- (BOOL)isUIKitPrivateView { - // 系统有些东西本身也存在不合理,但我们不关心这种,所以过滤掉 - if ([self isKindOfClass:[UIWindow class]]) return YES; - - __block BOOL isPrivate = NO; - NSString *classString = NSStringFromClass(self.class); - [@[@"LayoutContainer", @"NavigationItemButton", @"NavigationItemView", @"SelectionGrabber", @"InputViewContent", @"InputSetContainer", @"TextFieldContentView"] enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - if (([classString hasPrefix:@"UI"] || [classString hasPrefix:@"_UI"]) && [classString containsString:obj]) { - isPrivate = YES; - *stop = YES; - } - }]; - return isPrivate; -} - -- (void)alertConvertValueWithView:(UIView *)view { - if (IS_DEBUG && ![self isUIKitPrivateView] && ![self hasSharedAncestorViewWithView:view]) { - QMUILog(@"UIView (QMUI)", @"进行坐标系转换运算的 %@ 和 %@ 不存在共同的父 view,可能导致运算结果不准确(特别是在横竖屏旋转时,如果两个 view 处于不同的 window,由于 window 旋转有先后顺序,可能转换时两个 window 的方向不一致,坐标就会错乱)", self, view); - } -} - @end @implementation UIView (QMUI_ViewController) @@ -405,6 +289,9 @@ - (BOOL)qmui_visible { if (self.window) { return YES; } + if ([self isKindOfClass:UIWindow.class]) { + return !!((UIWindow *)self).windowScene; + } UIViewController *viewController = self.qmui_viewController; return viewController.qmui_visibleState >= QMUIViewControllerWillAppear && viewController.qmui_visibleState < QMUIViewControllerWillDisappear; } @@ -413,7 +300,7 @@ - (BOOL)qmui_visible { - (void)setQmui_viewController:(__kindof UIViewController * _Nullable)qmui_viewController { QMUIWeakObjectContainer *weakContainer = objc_getAssociatedObject(self, &kAssociatedObjectKey_viewController); if (!weakContainer) { - weakContainer = [QMUIWeakObjectContainer new]; + weakContainer = [[QMUIWeakObjectContainer alloc] init]; } weakContainer.object = qmui_viewController; objc_setAssociatedObject(self, &kAssociatedObjectKey_viewController, weakContainer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); @@ -440,13 +327,7 @@ + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ ExtendImplementationOfVoidMethodWithoutArguments([UIViewController class], @selector(viewDidLoad), ^(UIViewController *selfObject) { - if (@available(iOS 11.0, *)) { - selfObject.view.qmui_viewController = selfObject; - } else { - // 临时修复 iOS 10.0.2 上在输入框内切换输入法可能引发死循环的 bug,待查 - // https://github.com/Tencent/QMUI_iOS/issues/471 - ((UIView *)[selfObject valueForKey:@"_view"]).qmui_viewController = selfObject; - } + selfObject.view.qmui_viewController = selfObject; }); }); } @@ -481,7 +362,9 @@ - (BOOL)qmui_hasOverrideUIKitMethod:(SEL)selector { [UIDatePicker class], [UIPickerView class], [UIVisualEffectView class], - [UIWebView class], + // Apple 不再接受使用了 UIWebView 的 App 提交,所以这里去掉 UIWebView + // https://github.com/Tencent/QMUI_iOS/issues/741 +// [UIWebView class], [UIWindow class], [UINavigationBar class], [UIToolbar class], @@ -503,288 +386,49 @@ - (BOOL)qmui_hasOverrideUIKitMethod:(SEL)selector { @end -@implementation UIView (QMUI_Border) +const CGFloat QMUIViewSelfSizingHeight = INFINITY; +const CGSize QMUIViewFixedSizeNone = {-1, -1}; -QMUISynthesizeIdStrongProperty(qmui_borderLayer, setQmui_borderLayer) +@implementation UIView (QMUI_Layout) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ExtendImplementationOfNonVoidMethodWithSingleArgument([UIView class], @selector(initWithFrame:), CGRect, UIView *, ^UIView *(UIView *selfObject, CGRect frame, UIView *originReturnValue) { - [selfObject setDefaultStyle]; - return originReturnValue; - }); - - ExtendImplementationOfNonVoidMethodWithSingleArgument([UIView class], @selector(initWithCoder:), NSCoder *, UIView *, ^UIView *(UIView *selfObject, NSCoder *aDecoder, UIView *originReturnValue) { - [selfObject setDefaultStyle]; - return originReturnValue; - }); - - ExtendImplementationOfVoidMethodWithSingleArgument([UIView class], @selector(layoutSublayersOfLayer:), CALayer *, ^(UIView *selfObject, CALayer *layer) { - if ((!selfObject.qmui_borderLayer && selfObject.qmui_borderPosition == QMUIViewBorderPositionNone) || (!selfObject.qmui_borderLayer && selfObject.qmui_borderWidth == 0)) { - return; - } - - if (selfObject.qmui_borderLayer && selfObject.qmui_borderPosition == QMUIViewBorderPositionNone && !selfObject.qmui_borderLayer.path) { - return; - } - - if (selfObject.qmui_borderLayer && selfObject.qmui_borderWidth == 0 && selfObject.qmui_borderLayer.lineWidth == 0) { - return; - } - - if (!selfObject.qmui_borderLayer) { - selfObject.qmui_borderLayer = [CAShapeLayer layer]; - selfObject.qmui_borderLayer.fillColor = UIColorClear.CGColor; - [selfObject.qmui_borderLayer qmui_removeDefaultAnimations]; - [selfObject.layer addSublayer:selfObject.qmui_borderLayer]; - } - selfObject.qmui_borderLayer.frame = selfObject.bounds; - - CGFloat borderWidth = selfObject.qmui_borderWidth; - selfObject.qmui_borderLayer.lineWidth = borderWidth; - selfObject.qmui_borderLayer.strokeColor = selfObject.qmui_borderColor.CGColor; - selfObject.qmui_borderLayer.lineDashPhase = selfObject.qmui_dashPhase; - selfObject.qmui_borderLayer.lineDashPattern = selfObject.qmui_dashPattern; - - UIBezierPath *path = nil; - - if (selfObject.qmui_borderPosition != QMUIViewBorderPositionNone) { - path = [UIBezierPath bezierPath]; - } - - CGFloat (^adjustsLocation)(CGFloat, CGFloat, CGFloat) = ^CGFloat(CGFloat inside, CGFloat center, CGFloat outside) { - return selfObject.qmui_borderLocation == QMUIViewBorderLocationInside ? inside : (selfObject.qmui_borderLocation == QMUIViewBorderLocationCenter ? center : outside); - }; - - CGFloat lineOffset = adjustsLocation(borderWidth / 2.0, 0, -borderWidth / 2.0); // 为了像素对齐而做的偏移 - CGFloat lineCapOffset = adjustsLocation(0, borderWidth / 2.0, borderWidth); // 两条相邻的边框连接的位置 - - BOOL shouldShowTopBorder = (selfObject.qmui_borderPosition & QMUIViewBorderPositionTop) == QMUIViewBorderPositionTop; - BOOL shouldShowLeftBorder = (selfObject.qmui_borderPosition & QMUIViewBorderPositionLeft) == QMUIViewBorderPositionLeft; - BOOL shouldShowBottomBorder = (selfObject.qmui_borderPosition & QMUIViewBorderPositionBottom) == QMUIViewBorderPositionBottom; - BOOL shouldShowRightBorder = (selfObject.qmui_borderPosition & QMUIViewBorderPositionRight) == QMUIViewBorderPositionRight; - - UIBezierPath *topPath = [UIBezierPath bezierPath]; - UIBezierPath *leftPath = [UIBezierPath bezierPath]; - UIBezierPath *bottomPath = [UIBezierPath bezierPath]; - UIBezierPath *rightPath = [UIBezierPath bezierPath]; - - if (selfObject.layer.qmui_originCornerRadius > 0) { - - CGFloat cornerRadius = selfObject.layer.qmui_originCornerRadius; + OverrideImplementation([UIView class], @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, CGRect frame) { - if (selfObject.layer.qmui_maskedCorners) { - if ((selfObject.layer.qmui_maskedCorners & QMUILayerMinXMinYCorner) == QMUILayerMinXMinYCorner) { - [topPath addArcWithCenter:CGPointMake(cornerRadius, cornerRadius) radius:cornerRadius - lineOffset startAngle:1.25 * M_PI endAngle:1.5 * M_PI clockwise:YES]; - [topPath addLineToPoint:CGPointMake(CGRectGetWidth(selfObject.bounds) - cornerRadius, lineOffset)]; - [leftPath addArcWithCenter:CGPointMake(cornerRadius, cornerRadius) radius:cornerRadius - lineOffset startAngle:-0.75 * M_PI endAngle:-1 * M_PI clockwise:NO]; - [leftPath addLineToPoint:CGPointMake(lineOffset, CGRectGetHeight(selfObject.bounds) - cornerRadius)]; - } else { - [topPath moveToPoint:CGPointMake(shouldShowLeftBorder ? -lineCapOffset : 0, lineOffset)]; - [topPath addLineToPoint:CGPointMake(CGRectGetWidth(selfObject.bounds) - cornerRadius, lineOffset)]; - [leftPath moveToPoint:CGPointMake(lineOffset, shouldShowTopBorder ? -lineCapOffset : 0)]; - [leftPath addLineToPoint:CGPointMake(lineOffset, CGRectGetHeight(selfObject.bounds) - cornerRadius)]; - } - if ((selfObject.layer.qmui_maskedCorners & QMUILayerMinXMaxYCorner) == QMUILayerMinXMaxYCorner) { - [leftPath addArcWithCenter:CGPointMake(cornerRadius, CGRectGetHeight(selfObject.bounds) - cornerRadius) radius:cornerRadius - lineOffset startAngle:-1 * M_PI endAngle:-1.25 * M_PI clockwise:NO]; - [bottomPath addArcWithCenter:CGPointMake(cornerRadius, CGRectGetHeight(selfObject.bounds) - cornerRadius) radius:cornerRadius - lineOffset startAngle:-1.25 * M_PI endAngle:-1.5 * M_PI clockwise:NO]; - [bottomPath addLineToPoint:CGPointMake(CGRectGetWidth(selfObject.bounds) - cornerRadius, CGRectGetHeight(selfObject.bounds) - lineOffset)]; - } else { - [leftPath addLineToPoint:CGPointMake(lineOffset, CGRectGetHeight(selfObject.bounds) + (shouldShowBottomBorder ? lineCapOffset : 0))]; - CGFloat y = CGRectGetHeight(selfObject.bounds) - lineOffset; - [bottomPath moveToPoint:CGPointMake(shouldShowLeftBorder ? -lineCapOffset : 0, y)]; - [bottomPath addLineToPoint:CGPointMake(CGRectGetWidth(selfObject.bounds) - cornerRadius, y)]; - } - if ((selfObject.layer.qmui_maskedCorners & QMUILayerMaxXMaxYCorner) == QMUILayerMaxXMaxYCorner) { - [bottomPath addArcWithCenter:CGPointMake(CGRectGetWidth(selfObject.bounds) - cornerRadius, CGRectGetHeight(selfObject.bounds) - cornerRadius) radius:cornerRadius - lineOffset startAngle:-1.5 * M_PI endAngle:-1.75 * M_PI clockwise:NO]; - [rightPath addArcWithCenter:CGPointMake(CGRectGetWidth(selfObject.bounds) - cornerRadius, CGRectGetHeight(selfObject.bounds) - cornerRadius) radius:cornerRadius - lineOffset startAngle:-1.75 * M_PI endAngle:-2 * M_PI clockwise:NO]; - [rightPath addLineToPoint:CGPointMake(CGRectGetWidth(selfObject.bounds) - lineOffset, cornerRadius)]; - } else { - CGFloat y = CGRectGetHeight(selfObject.bounds) - lineOffset; - [bottomPath addLineToPoint:CGPointMake(CGRectGetWidth(selfObject.bounds) + (shouldShowRightBorder ? lineCapOffset : 0), y)]; - CGFloat x = CGRectGetWidth(selfObject.bounds) - lineOffset; - [rightPath moveToPoint:CGPointMake(x, CGRectGetHeight(selfObject.bounds) + (shouldShowBottomBorder ? lineCapOffset : 0))]; - [rightPath addLineToPoint:CGPointMake(x, cornerRadius)]; - } - if ((selfObject.layer.qmui_maskedCorners & QMUILayerMaxXMinYCorner) == QMUILayerMaxXMinYCorner) { - [rightPath addArcWithCenter:CGPointMake(CGRectGetWidth(selfObject.bounds) - cornerRadius, cornerRadius) radius:cornerRadius - lineOffset startAngle:0 * M_PI endAngle:-0.25 * M_PI clockwise:NO]; - [topPath addArcWithCenter:CGPointMake(CGRectGetWidth(selfObject.bounds) - cornerRadius, cornerRadius) radius:cornerRadius - lineOffset startAngle:1.5 * M_PI endAngle:1.75 * M_PI clockwise:YES]; - } else { - CGFloat x = CGRectGetWidth(selfObject.bounds) - lineOffset; - [rightPath addLineToPoint:CGPointMake(x, shouldShowTopBorder ? -lineCapOffset : 0)]; - [topPath addLineToPoint:CGPointMake(CGRectGetWidth(selfObject.bounds) + (shouldShowRightBorder ? lineCapOffset : 0), lineOffset)]; - } - } else { - [topPath addArcWithCenter:CGPointMake(cornerRadius, cornerRadius) radius:cornerRadius - lineOffset startAngle:1.25 * M_PI endAngle:1.5 * M_PI clockwise:YES]; - [topPath addLineToPoint:CGPointMake(CGRectGetWidth(selfObject.bounds) - cornerRadius, lineOffset)]; - [topPath addArcWithCenter:CGPointMake(CGRectGetWidth(selfObject.bounds) - cornerRadius, cornerRadius) radius:cornerRadius - lineOffset startAngle:1.5 * M_PI endAngle:1.75 * M_PI clockwise:YES]; - - [leftPath addArcWithCenter:CGPointMake(cornerRadius, cornerRadius) radius:cornerRadius - lineOffset startAngle:-0.75 * M_PI endAngle:-1 * M_PI clockwise:NO]; - [leftPath addLineToPoint:CGPointMake(lineOffset, CGRectGetHeight(selfObject.bounds) - cornerRadius)]; - [leftPath addArcWithCenter:CGPointMake(cornerRadius, CGRectGetHeight(selfObject.bounds) - cornerRadius) radius:cornerRadius - lineOffset startAngle:-1 * M_PI endAngle:-1.25 * M_PI clockwise:NO]; - - [bottomPath addArcWithCenter:CGPointMake(cornerRadius, CGRectGetHeight(selfObject.bounds) - cornerRadius) radius:cornerRadius - lineOffset startAngle:-1.25 * M_PI endAngle:-1.5 * M_PI clockwise:NO]; - [bottomPath addLineToPoint:CGPointMake(CGRectGetWidth(selfObject.bounds) - cornerRadius, CGRectGetHeight(selfObject.bounds) - lineOffset)]; - [bottomPath addArcWithCenter:CGPointMake(CGRectGetHeight(selfObject.bounds) - cornerRadius, CGRectGetHeight(selfObject.bounds) - cornerRadius) radius:cornerRadius - lineOffset startAngle:-1.5 * M_PI endAngle:-1.75 * M_PI clockwise:NO]; - - [rightPath addArcWithCenter:CGPointMake(CGRectGetWidth(selfObject.bounds) - cornerRadius, CGRectGetHeight(selfObject.bounds) - cornerRadius) radius:cornerRadius - lineOffset startAngle:-1.75 * M_PI endAngle:-2 * M_PI clockwise:NO]; - [rightPath addLineToPoint:CGPointMake(CGRectGetWidth(selfObject.bounds) - lineOffset, cornerRadius)]; - [rightPath addArcWithCenter:CGPointMake(CGRectGetWidth(selfObject.bounds) - cornerRadius, cornerRadius) radius:cornerRadius - lineOffset startAngle:0 * M_PI endAngle:-0.25 * M_PI clockwise:NO]; + if (!CGSizeEqualToSize(selfObject.qmui_fixedSize, QMUIViewFixedSizeNone)) { + frame.size = selfObject.qmui_fixedSize; } - } else { - [topPath moveToPoint:CGPointMake(shouldShowLeftBorder ? -lineCapOffset : 0, lineOffset)]; - [topPath addLineToPoint:CGPointMake(CGRectGetWidth(selfObject.bounds) + (shouldShowRightBorder ? lineCapOffset : 0), lineOffset)]; + // QMUIViewSelfSizingHeight 的功能 + if (frame.size.width > 0 && isinf(frame.size.height)) { + CGFloat height = flat([selfObject sizeThatFits:CGSizeMake(CGRectGetWidth(frame), CGFLOAT_MAX)].height); + frame = CGRectSetHeight(frame, height); + } - [leftPath moveToPoint:CGPointMake(lineOffset, shouldShowTopBorder ? -lineCapOffset : 0)]; - [leftPath addLineToPoint:CGPointMake(lineOffset, CGRectGetHeight(selfObject.bounds) + (shouldShowBottomBorder ? lineCapOffset : 0))]; + // 对非法的 frame,Debug 下中 assert,Release 下会将其中的 NaN 改为 0,避免 crash + if (CGRectIsNaN(frame)) { + QMUIAssert(NO, @"UIView (QMUI)", @"%@ setFrame:%@,参数包含 NaN,已被拦截并处理为 0。%@", selfObject, NSStringFromCGRect(frame), [NSThread callStackSymbols]); + if (!IS_DEBUG) { + frame = CGRectSafeValue(frame); + } + } - CGFloat y = CGRectGetHeight(selfObject.bounds) - lineOffset; - [bottomPath moveToPoint:CGPointMake(shouldShowLeftBorder ? -lineCapOffset : 0, y)]; - [bottomPath addLineToPoint:CGPointMake(CGRectGetWidth(selfObject.bounds) + (shouldShowRightBorder ? lineCapOffset : 0), y)]; + CGRect precedingFrame = selfObject.frame; + BOOL valueChange = !CGRectEqualToRect(frame, precedingFrame); + if (selfObject.qmui_frameWillChangeBlock && valueChange) { + frame = selfObject.qmui_frameWillChangeBlock(selfObject, frame); + } - CGFloat x = CGRectGetWidth(selfObject.bounds) - lineOffset; - [rightPath moveToPoint:CGPointMake(x, CGRectGetHeight(selfObject.bounds) + (shouldShowBottomBorder ? lineCapOffset : 0))]; - [rightPath addLineToPoint:CGPointMake(x, shouldShowTopBorder ? -lineCapOffset : 0)]; - } - - if (shouldShowTopBorder && ![topPath isEmpty]) { - [path appendPath:topPath]; - } - if (shouldShowLeftBorder && ![leftPath isEmpty]) { - [path appendPath:leftPath]; - } - if (shouldShowBottomBorder && ![bottomPath isEmpty]) { - [path appendPath:bottomPath]; - } - if (shouldShowRightBorder && ![rightPath isEmpty]) { - [path appendPath:rightPath]; - } - - selfObject.qmui_borderLayer.path = path.CGPath; - }); - }); -} - -- (void)setDefaultStyle { - self.qmui_borderWidth = PixelOne; - self.qmui_borderColor = UIColorSeparator; -} - -static char kAssociatedObjectKey_borderLocation; -- (void)setQmui_borderLocation:(QMUIViewBorderLocation)qmui_borderLocation { - objc_setAssociatedObject(self, &kAssociatedObjectKey_borderLocation, @(qmui_borderLocation), OBJC_ASSOCIATION_RETAIN_NONATOMIC); - [self setNeedsLayout]; -} - -- (QMUIViewBorderLocation)qmui_borderLocation { - return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_borderLocation)) unsignedIntegerValue]; -} - -static char kAssociatedObjectKey_borderPosition; -- (void)setQmui_borderPosition:(QMUIViewBorderPosition)qmui_borderPosition { - objc_setAssociatedObject(self, &kAssociatedObjectKey_borderPosition, @(qmui_borderPosition), OBJC_ASSOCIATION_RETAIN_NONATOMIC); - [self setNeedsLayout]; -} - -- (QMUIViewBorderPosition)qmui_borderPosition { - return (QMUIViewBorderPosition)[objc_getAssociatedObject(self, &kAssociatedObjectKey_borderPosition) unsignedIntegerValue]; -} - -static char kAssociatedObjectKey_borderWidth; -- (void)setQmui_borderWidth:(CGFloat)qmui_borderWidth { - objc_setAssociatedObject(self, &kAssociatedObjectKey_borderWidth, @(qmui_borderWidth), OBJC_ASSOCIATION_RETAIN_NONATOMIC); - [self setNeedsLayout]; -} - -- (CGFloat)qmui_borderWidth { - return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_borderWidth)) qmui_CGFloatValue]; -} - -static char kAssociatedObjectKey_borderColor; -- (void)setQmui_borderColor:(UIColor *)qmui_borderColor { - objc_setAssociatedObject(self, &kAssociatedObjectKey_borderColor, qmui_borderColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - [self setNeedsLayout]; -} - -- (UIColor *)qmui_borderColor { - return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_borderColor); -} - -static char kAssociatedObjectKey_dashPhase; -- (void)setQmui_dashPhase:(CGFloat)qmui_dashPhase { - objc_setAssociatedObject(self, &kAssociatedObjectKey_dashPhase, @(qmui_dashPhase), OBJC_ASSOCIATION_RETAIN_NONATOMIC); - [self setNeedsLayout]; -} - -- (CGFloat)qmui_dashPhase { - return [(NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_dashPhase) qmui_CGFloatValue]; -} - -static char kAssociatedObjectKey_dashPattern; -- (void)setQmui_dashPattern:(NSArray *)qmui_dashPattern { - objc_setAssociatedObject(self, &kAssociatedObjectKey_dashPattern, qmui_dashPattern, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - [self setNeedsLayout]; -} - -- (NSArray *)qmui_dashPattern { - return (NSArray *)objc_getAssociatedObject(self, &kAssociatedObjectKey_dashPattern); -} - -@end - - -const CGFloat QMUIViewSelfSizingHeight = INFINITY; - -@implementation UIView (QMUI_Layout) - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - - OverrideImplementation([UIView class], @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UIView *selfObject, CGRect frame) { // call super - void (^callSuperBlock)(CGRect) = ^void(CGRect aFrame) { - void (*originSelectorIMP)(id, SEL, CGRect); - originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, aFrame); - }; + void (*originSelectorIMP)(id, SEL, CGRect); + originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, frame); - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - // QMUIViewSelfSizingHeight 的功能 - if (CGRectGetWidth(frame) > 0 && isinf(CGRectGetHeight(frame))) { - CGFloat height = flat([selfObject sizeThatFits:CGSizeMake(CGRectGetWidth(frame), CGFLOAT_MAX)].height); - frame = CGRectSetHeight(frame, height); - } - - // 对非法的 frame,Debug 下中 assert,Release 下会将其中的 NaN 改为 0,避免 crash - if (CGRectIsNaN(frame)) { - QMUILogWarn(@"UIView (QMUI)", @"%@ setFrame:%@,参数包含 NaN,已被拦截并处理为 0。%@", selfObject, NSStringFromCGRect(frame), [NSThread callStackSymbols]); - if (QMUICMIActivated && !ShouldPrintQMUIWarnLogToConsole) { - NSAssert(NO, @"UIView setFrame: 出现 NaN"); - } - if (!IS_DEBUG) { - frame = CGRectSafeValue(frame); - } - } - - CGRect precedingFrame = selfObject.frame; - BOOL valueChange = !CGRectEqualToRect(frame, precedingFrame); - if (selfObject.qmui_frameWillChangeBlock && valueChange) { - frame = selfObject.qmui_frameWillChangeBlock(selfObject, frame); - } - - callSuperBlock(frame); - - if (selfObject.qmui_frameDidChangeBlock && valueChange) { - selfObject.qmui_frameDidChangeBlock(selfObject, precedingFrame); - } - } else { - callSuperBlock(frame); + if (selfObject.qmui_frameDidChangeBlock && valueChange) { + selfObject.qmui_frameDidChangeBlock(selfObject, precedingFrame); } }; }); @@ -792,31 +436,26 @@ + (void)load { OverrideImplementation([UIView class], @selector(setBounds:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIView *selfObject, CGRect bounds) { + if (!CGSizeEqualToSize(selfObject.qmui_fixedSize, QMUIViewFixedSizeNone)) { + bounds.size = selfObject.qmui_fixedSize; + } + + CGRect precedingFrame = selfObject.frame; + CGRect precedingBounds = selfObject.bounds; + BOOL valueChange = !CGSizeEqualToSize(bounds.size, precedingBounds.size);// bounds 只有 size 发生变化才会影响 frame + if (selfObject.qmui_frameWillChangeBlock && valueChange) { + CGRect followingFrame = CGRectMake(CGRectGetMinX(precedingFrame) + CGFloatGetCenter(CGRectGetWidth(bounds), CGRectGetWidth(precedingFrame)), CGRectGetMinY(precedingFrame) + CGFloatGetCenter(CGRectGetHeight(bounds), CGRectGetHeight(precedingFrame)), bounds.size.width, bounds.size.height); + followingFrame = selfObject.qmui_frameWillChangeBlock(selfObject, followingFrame); + bounds = CGRectSetSize(bounds, followingFrame.size); + } + // call super - void (^callSuperBlock)(CGRect) = ^void(CGRect aBounds) { - void (*originSelectorIMP)(id, SEL, CGRect); - originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, aBounds); - }; + void (*originSelectorIMP)(id, SEL, CGRect); + originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, bounds); - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - CGRect precedingFrame = selfObject.frame; - CGRect precedingBounds = selfObject.bounds; - BOOL valueChange = !CGSizeEqualToSize(bounds.size, precedingBounds.size);// bounds 只有 size 发生变化才会影响 frame - if (selfObject.qmui_frameWillChangeBlock && valueChange) { - CGRect followingFrame = CGRectMake(CGRectGetMinX(precedingFrame) + CGFloatGetCenter(CGRectGetWidth(bounds), CGRectGetWidth(precedingFrame)), CGRectGetMinY(precedingFrame) + CGFloatGetCenter(CGRectGetHeight(bounds), CGRectGetHeight(precedingFrame)), bounds.size.width, bounds.size.height); - followingFrame = selfObject.qmui_frameWillChangeBlock(selfObject, followingFrame); - bounds = CGRectSetSize(bounds, followingFrame.size); - } - - callSuperBlock(bounds); - - if (selfObject.qmui_frameDidChangeBlock && valueChange) { - selfObject.qmui_frameDidChangeBlock(selfObject, precedingFrame); - } - } else { - callSuperBlock(bounds); + if (selfObject.qmui_frameDidChangeBlock && valueChange) { + selfObject.qmui_frameDidChangeBlock(selfObject, precedingFrame); } }; }); @@ -824,31 +463,22 @@ + (void)load { OverrideImplementation([UIView class], @selector(setCenter:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIView *selfObject, CGPoint center) { + CGRect precedingFrame = selfObject.frame; + CGPoint precedingCenter = selfObject.center; + BOOL valueChange = !CGPointEqualToPoint(center, precedingCenter); + if (selfObject.qmui_frameWillChangeBlock && valueChange) { + CGRect followingFrame = CGRectSetXY(precedingFrame, center.x - CGRectGetWidth(selfObject.frame) / 2, center.y - CGRectGetHeight(selfObject.frame) / 2); + followingFrame = selfObject.qmui_frameWillChangeBlock(selfObject, followingFrame); + center = CGPointMake(CGRectGetMidX(followingFrame), CGRectGetMidY(followingFrame)); + } + // call super - void (^callSuperBlock)(CGPoint) = ^void(CGPoint aCenter) { - void (*originSelectorIMP)(id, SEL, CGPoint); - originSelectorIMP = (void (*)(id, SEL, CGPoint))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, aCenter); - }; + void (*originSelectorIMP)(id, SEL, CGPoint); + originSelectorIMP = (void (*)(id, SEL, CGPoint))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, center); - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - CGRect precedingFrame = selfObject.frame; - CGPoint precedingCenter = selfObject.center; - BOOL valueChange = !CGPointEqualToPoint(center, precedingCenter); - if (selfObject.qmui_frameWillChangeBlock && valueChange) { - CGRect followingFrame = CGRectSetXY(precedingFrame, center.x - CGRectGetWidth(selfObject.frame) / 2, center.y - CGRectGetHeight(selfObject.frame) / 2); - followingFrame = selfObject.qmui_frameWillChangeBlock(selfObject, followingFrame); - center = CGPointMake(CGRectGetMidX(followingFrame), CGRectGetMidY(followingFrame)); - } - - callSuperBlock(center); - - if (selfObject.qmui_frameDidChangeBlock && valueChange) { - selfObject.qmui_frameDidChangeBlock(selfObject, precedingFrame); - } - } else { - callSuperBlock(center); + if (selfObject.qmui_frameDidChangeBlock && valueChange) { + selfObject.qmui_frameDidChangeBlock(selfObject, precedingFrame); } }; }); @@ -856,30 +486,21 @@ + (void)load { OverrideImplementation([UIView class], @selector(setTransform:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIView *selfObject, CGAffineTransform transform) { + CGRect precedingFrame = selfObject.frame; + CGAffineTransform precedingTransform = selfObject.transform; + BOOL valueChange = !CGAffineTransformEqualToTransform(transform, precedingTransform); + if (selfObject.qmui_frameWillChangeBlock && valueChange) { + CGRect followingFrame = CGRectApplyAffineTransformWithAnchorPoint(precedingFrame, transform, selfObject.layer.anchorPoint); + selfObject.qmui_frameWillChangeBlock(selfObject, followingFrame);// 对于 CGAffineTransform,无法根据修改后的 rect 来算出新的 transform,所以就不修改 transform 的值了 + } + // call super - void (^callSuperBlock)(CGAffineTransform) = ^void(CGAffineTransform aTransform) { - void (*originSelectorIMP)(id, SEL, CGAffineTransform); - originSelectorIMP = (void (*)(id, SEL, CGAffineTransform))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, aTransform); - }; + void (*originSelectorIMP)(id, SEL, CGAffineTransform); + originSelectorIMP = (void (*)(id, SEL, CGAffineTransform))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, transform); - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - CGRect precedingFrame = selfObject.frame; - CGAffineTransform precedingTransform = selfObject.transform; - BOOL valueChange = !CGAffineTransformEqualToTransform(transform, precedingTransform); - if (selfObject.qmui_frameWillChangeBlock && valueChange) { - CGRect followingFrame = CGRectApplyAffineTransformWithAnchorPoint(precedingFrame, transform, selfObject.layer.anchorPoint); - selfObject.qmui_frameWillChangeBlock(selfObject, followingFrame);// 对于 CGAffineTransform,无法根据修改后的 rect 来算出新的 transform,所以就不修改 transform 的值了 - } - - callSuperBlock(transform); - - if (selfObject.qmui_frameDidChangeBlock && valueChange) { - selfObject.qmui_frameDidChangeBlock(selfObject, precedingFrame); - } - } else { - callSuperBlock(transform); + if (selfObject.qmui_frameDidChangeBlock && valueChange) { + selfObject.qmui_frameDidChangeBlock(selfObject, precedingFrame); } }; }); @@ -918,6 +539,14 @@ - (void)setQmui_right:(CGFloat)right { self.frame = CGRectSetX(self.frame, right - CGRectGetWidth(self.frame)); } +- (CGPoint)qmui_origin { + return self.frame.origin; +} + +- (void)setQmui_origin:(CGPoint)qmui_origin { + self.center = CGPointMake(qmui_origin.x + CGRectGetWidth(self.frame) / 2, qmui_origin.y + CGRectGetHeight(self.frame) / 2); +} + - (CGFloat)qmui_width { return CGRectGetWidth(self.frame); } @@ -934,6 +563,39 @@ - (void)setQmui_height:(CGFloat)height { self.frame = CGRectSetHeight(self.frame, height); } +- (CGSize)qmui_size { + return self.frame.size; +} + +- (void)setQmui_size:(CGSize)qmui_size { + self.frame = CGRectSetSize(self.frame, qmui_size); +} + +static char kAssociatedObjectKey_fixedSize; +- (void)setQmui_fixedSize:(CGSize)qmui_fixedSize { + objc_setAssociatedObject(self, &kAssociatedObjectKey_fixedSize, @(qmui_fixedSize), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + if (!CGSizeEqualToSize(qmui_fixedSize, QMUIViewFixedSizeNone)) { + self.qmui_sizeThatFitsBlock = ^CGSize(__kindof UIView * _Nonnull view, CGSize size, CGSize superResult) { + if (!CGSizeEqualToSize(view.qmui_fixedSize, QMUIViewFixedSizeNone)) { + return view.qmui_fixedSize; + } + return superResult; + }; + self.qmui_size = qmui_fixedSize; + } else { + self.qmui_sizeThatFitsBlock = nil; + } +} + +- (CGSize)qmui_fixedSize { + NSNumber *result = objc_getAssociatedObject(self, &kAssociatedObjectKey_fixedSize); + if (!result) { + return QMUIViewFixedSizeNone; + } + return result.CGSizeValue; +} + - (CGFloat)qmui_extendToTop { return self.qmui_top; } @@ -1018,43 +680,102 @@ - (UIImage *)qmui_snapshotImageAfterScreenUpdates:(BOOL)afterScreenUpdates { @implementation UIView (QMUI_Debug) QMUISynthesizeBOOLProperty(qmui_needsDifferentDebugColor, setQmui_needsDifferentDebugColor) -QMUISynthesizeBOOLProperty(qmui_hasDebugColor, setQmui_hasDebugColor) - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - ExtendImplementationOfVoidMethodWithoutArguments([UIView class], @selector(layoutSubviews), ^(UIView *selfObject) { - if (selfObject.qmui_shouldShowDebugColor) { - selfObject.qmui_hasDebugColor = YES; - selfObject.backgroundColor = [selfObject debugColor]; - [selfObject renderColorWithSubviews:selfObject.subviews]; - } - }); - }); -} static char kAssociatedObjectKey_shouldShowDebugColor; - (void)setQmui_shouldShowDebugColor:(BOOL)qmui_shouldShowDebugColor { objc_setAssociatedObject(self, &kAssociatedObjectKey_shouldShowDebugColor, @(qmui_shouldShowDebugColor), OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (qmui_shouldShowDebugColor) { - [self setNeedsLayout]; + [QMUIHelper executeBlock:^{ + ExtendImplementationOfVoidMethodWithoutArguments([UIView class], @selector(layoutSubviews), ^(UIView *selfObject) { + if (selfObject.qmui_shouldShowDebugColor) { + selfObject.backgroundColor = [selfObject debugColor]; + [selfObject renderColorWithSubviews:selfObject.subviews]; + } else if (objc_getAssociatedObject(selfObject, &kAssociatedObjectKey_shouldShowDebugColor)) { + // 设置过 qmui_shouldShowDebugColor,但当前的值为 NO 的情况,则无脑清空所有背景色(可能会把业务自己设置的背景色去掉,由于是调试功能,无所谓) + selfObject.backgroundColor = UIColor.clearColor; + [selfObject renderColorWithSubviews:selfObject.subviews]; + } + }); + } oncePerIdentifier:@"UIView (QMUIDebug) shouldShowDebugColor"]; } + [self setNeedsLayout]; } - (BOOL)qmui_shouldShowDebugColor { BOOL flag = [objc_getAssociatedObject(self, &kAssociatedObjectKey_shouldShowDebugColor) boolValue]; return flag; } +static char kAssociatedObjectKey_layoutSubviewsBlock; +- (void)setQmui_layoutSubviewsBlock:(void (^)(__kindof UIView * _Nonnull))qmui_layoutSubviewsBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_layoutSubviewsBlock, qmui_layoutSubviewsBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + Class viewClass = self.class; + [QMUIHelper executeBlock:^{ + // iOS 14 及以上,iPad 悬浮键盘,项目里 hook 了 -[UIView layoutSubviews] 的同时为输入框设置 inputAccessoryView,则输入框聚焦时会触发系统布局死循环 + // 实测只有 iOS 14 有这种问题,iOS 13、15 都没有,但现网又有用户反馈 iOS 15 也有问题,暂且放开 iOS 15 + // https://github.com/Tencent/QMUI_iOS/issues/1247 + // https://km.woa.com/group/24897/articles/show/456340 + if (IOS_VERSION >= 14.0 && IS_IPAD && viewClass == UIView.class) { + IMP layoutSubviewsIMPForUIKit = class_getMethodImplementation(UIView.class, @selector(layoutSubviews)); + SEL layoutSubviewSEL = @selector(layoutSubviews); + const char * typeEncoding = method_getTypeEncoding(class_getInstanceMethod(UIView.class, layoutSubviewSEL)); + class_addMethod(NSClassFromString(@"UIInputSetHostView"), layoutSubviewSEL, layoutSubviewsIMPForUIKit, typeEncoding); + } + ExtendImplementationOfVoidMethodWithoutArguments(viewClass, @selector(layoutSubviews), ^(__kindof UIView *selfObject) { + if (selfObject.qmui_layoutSubviewsBlock && [selfObject isMemberOfClass:viewClass]) { + selfObject.qmui_layoutSubviewsBlock(selfObject); + } + }); + } oncePerIdentifier:[NSString stringWithFormat:@"UIView %@-%@", NSStringFromClass(viewClass), NSStringFromSelector(@selector(layoutSubviews))]]; +} + +- (void (^)(UIView * _Nonnull))qmui_layoutSubviewsBlock { + return objc_getAssociatedObject(self, &kAssociatedObjectKey_layoutSubviewsBlock); +} + +static char kAssociatedObjectKey_sizeThatFitsBlock; +- (void)setQmui_sizeThatFitsBlock:(CGSize (^)(__kindof UIView * _Nonnull, CGSize, CGSize))qmui_sizeThatFitsBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_sizeThatFitsBlock, qmui_sizeThatFitsBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + + if (!qmui_sizeThatFitsBlock) return; + + // Extend 每个实例对象的类是为了保证比子类的 sizeThatFits 逻辑要更晚调用 + Class viewClass = self.class; + [QMUIHelper executeBlock:^{ + OverrideImplementation(viewClass, @selector(sizeThatFits:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGSize(UIView *selfObject, CGSize firstArgv) { + + // call super + CGSize (*originSelectorIMP)(id, SEL, CGSize); + originSelectorIMP = (CGSize (*)(id, SEL, CGSize))originalIMPProvider(); + CGSize result = originSelectorIMP(selfObject, originCMD, firstArgv); + + if (selfObject.qmui_sizeThatFitsBlock && [selfObject isMemberOfClass:viewClass]) { + result = selfObject.qmui_sizeThatFitsBlock(selfObject, firstArgv, result); + } + return result; + }; + }); + } oncePerIdentifier:[NSString stringWithFormat:@"UIView %@-%@", NSStringFromClass(viewClass), NSStringFromSelector(@selector(sizeThatFits:))]]; +} + +- (CGSize (^)(__kindof UIView * _Nonnull, CGSize, CGSize))qmui_sizeThatFitsBlock { + return objc_getAssociatedObject(self, &kAssociatedObjectKey_sizeThatFitsBlock); +} + - (void)renderColorWithSubviews:(NSArray *)subviews { + // 只处理第一级 subviews for (UIView *view in subviews) { if ([view isKindOfClass:[UIStackView class]]) { UIStackView *stackView = (UIStackView *)view; [self renderColorWithSubviews:stackView.arrangedSubviews]; } - view.qmui_hasDebugColor = YES; view.qmui_shouldShowDebugColor = self.qmui_shouldShowDebugColor; view.qmui_needsDifferentDebugColor = self.qmui_needsDifferentDebugColor; - view.backgroundColor = [self debugColor]; + if (view.qmui_shouldShowDebugColor) { + view.backgroundColor = [view debugColor]; + } else { + view.backgroundColor = UIColor.clearColor; + } } } diff --git a/QMUI/QMUIKit/UIKitExtensions/UIView+QMUIBorder.h b/QMUI/QMUIKit/UIKitExtensions/UIView+QMUIBorder.h new file mode 100644 index 00000000..41e45216 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIView+QMUIBorder.h @@ -0,0 +1,73 @@ +/** + * 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. + */ + +// +// UIView+QMUIBorder.h +// QMUIKit +// +// Created by MoLice on 2020/6/28. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_OPTIONS(NSUInteger, QMUIViewBorderPosition) { + QMUIViewBorderPositionNone = 0, + QMUIViewBorderPositionTop = 1 << 0, + QMUIViewBorderPositionLeft = 1 << 1, + QMUIViewBorderPositionBottom = 1 << 2, + QMUIViewBorderPositionRight = 1 << 3 +}; + +typedef NS_ENUM(NSUInteger, QMUIViewBorderLocation) { + QMUIViewBorderLocationInside, + QMUIViewBorderLocationCenter, + QMUIViewBorderLocationOutside +}; + +/** +* UIView (QMUIBorder) 为 UIView 方便地显示某几个方向上的边框。 +* +* 系统的默认实现里,要为 UIView 加边框一般是通过 view.layer 来实现,view.layer 会给四条边都加上边框,如果你只想为其中某几条加上边框就很麻烦,于是 UIView (QMUIBorder) 提供了 qmui_borderPosition 来解决这个问题。 +* @warning 注意如果你需要为 UIView 四条边都加上边框,请使用系统默认的 view.layer 来实现,而不要用 UIView (QMUIBorder),会浪费资源,这也是为什么 QMUIViewBorderPosition 不提供一个 QMUIViewBorderPositionAll 枚举值的原因。 +*/ +@interface UIView (QMUIBorder) + +/// 设置边框的位置,默认为 QMUIViewBorderLocationInside,与 view.layer.border 一致。 +@property(nonatomic, assign) QMUIViewBorderLocation qmui_borderLocation; + +/// 设置边框类型,支持组合,例如:`borderPosition = QMUIViewBorderPositionTop|QMUIViewBorderPositionBottom`。默认为 QMUIViewBorderPositionNone。 +@property(nonatomic, assign) QMUIViewBorderPosition qmui_borderPosition; + +/// 边框的大小,默认为PixelOne。请注意修改 qmui_borderPosition 的值以将边框显示出来。 +@property(nonatomic, assign) IBInspectable CGFloat qmui_borderWidth; + +/** + 边框的偏移,默认为 UIEdgeInsetsZero,当某个方向的值为正值,则边框会往内缩,负值则边框会往外拓。但对于不同的边框线,borderInsets 的 top/left/bottom/right 会对应不同的方向,具体如下: + 1. 对于 QMUIViewBorderPositionTop 而言,边框从左往右绘制。所以 left 正值则边框的左端点往右缩(右端点不变),right 正值则边框的右端点往左缩(左端点不变)。top 正值则边框往下偏移,bottom 正值则边框往上偏移。 + 2. 对于 QMUIViewBorderPositionLeft 而言,边框从下往上绘制。所以 left 正值则边框的底端点往上缩(顶端点不变),right 正值则边框的顶端点往底缩(底端点不变)。top 正值则边框往右偏移,bottom 正值则边框往左偏移。 + 3. 对于 QMUIViewBorderPositionBottom 而言,边框从右下往左下绘制。所以 left 正值则边框的右下端点往左缩(左端点不变),right 正值则边框的左下端点往右缩(右端点不变)。top 正值则边框往上偏移,bottom 正值则边框往下偏移。 + 4. 对于 QMUIViewBorderPositionRight 而言,边框从上往下绘制。所以 left 正值则边框的顶端点往下缩(底端点不变),right 正值则边框的底端点往上缩(顶端点不变)。top 正值则边框往左偏移,bottom 正值则边框往右偏移。 + */ +@property(nonatomic, assign) IBInspectable UIEdgeInsets qmui_borderInsets; + +/// 边框的颜色,默认为UIColorSeparator。请注意修改 qmui_borderPosition 的值以将边框显示出来。 +@property(nullable, nonatomic, strong) IBInspectable UIColor *qmui_borderColor; + +/// 虚线 : dashPhase默认是0,且当dashPattern设置了才有效 +/// qmui_dashPhase 表示虚线起始的偏移,qmui_dashPattern 可以传一个数组,表示“lineWidth,lineSpacing,lineWidth,lineSpacing...”的顺序,至少传 2 个。 +@property(nonatomic, assign) CGFloat qmui_dashPhase; +@property(nullable, nonatomic, copy) NSArray *qmui_dashPattern; + +/// border的layer +@property(nullable, nonatomic, strong, readonly) CAShapeLayer *qmui_borderLayer; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIView+QMUIBorder.m b/QMUI/QMUIKit/UIKitExtensions/UIView+QMUIBorder.m new file mode 100644 index 00000000..3f761d83 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIView+QMUIBorder.m @@ -0,0 +1,353 @@ +/** + * 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. + */ + +// +// UIView+QMUIBorder.m +// QMUIKit +// +// Created by MoLice on 2020/6/28. +// + +#import "UIView+QMUIBorder.h" +#import "QMUICore.h" +#import "CALayer+QMUI.h" + +@interface QMUIBorderLayer : CAShapeLayer + +@property(nonatomic, weak) UIView *_qmuibd_targetBorderView; +@end + +@implementation UIView (QMUIBorder) + +QMUISynthesizeIdStrongProperty(qmui_borderLayer, setQmui_borderLayer) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + ExtendImplementationOfNonVoidMethodWithSingleArgument([UIView class], @selector(initWithFrame:), CGRect, UIView *, ^UIView *(UIView *selfObject, CGRect frame, UIView *originReturnValue) { + [selfObject _qmuibd_setDefaultStyle]; + return originReturnValue; + }); + + ExtendImplementationOfNonVoidMethodWithSingleArgument([UIView class], @selector(initWithCoder:), NSCoder *, UIView *, ^UIView *(UIView *selfObject, NSCoder *aDecoder, UIView *originReturnValue) { + [selfObject _qmuibd_setDefaultStyle]; + return originReturnValue; + }); + }); +} + +- (void)_qmuibd_setDefaultStyle { + self.qmui_borderWidth = PixelOne; + self.qmui_borderColor = UIColorSeparator; +} + +- (void)_qmuibd_createBorderLayerIfNeeded { + BOOL shouldShowBorder = self.qmui_borderWidth > 0 && self.qmui_borderColor && self.qmui_borderPosition != QMUIViewBorderPositionNone; + if (!shouldShowBorder) { + self.qmui_borderLayer.hidden = YES; + return; + } + + [QMUIHelper executeBlock:^{ + OverrideImplementation([UIView class], @selector(layoutSublayersOfLayer:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, CALayer *firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, CALayer *); + originSelectorIMP = (void (*)(id, SEL, CALayer *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if (!selfObject.qmui_borderLayer || selfObject.qmui_borderLayer.hidden) return; + selfObject.qmui_borderLayer.frame = selfObject.bounds; + [selfObject.layer qmui_bringSublayerToFront:selfObject.qmui_borderLayer]; + [selfObject.qmui_borderLayer setNeedsLayout];// 把布局刷新逻辑剥离到 layer 内,方便在子线程里直接刷新 layer,如果放在 UIView 内,子线程里就无法主动请求刷新了 + }; + }); + } oncePerIdentifier:@"UIView (QMUIBorder) layoutSublayers"]; + + if (!self.qmui_borderLayer) { + QMUIBorderLayer *layer = [QMUIBorderLayer layer]; + layer._qmuibd_targetBorderView = self; + [layer qmui_removeDefaultAnimations]; + layer.fillColor = UIColorClear.CGColor; + [self.layer addSublayer:layer]; + self.qmui_borderLayer = layer; + } + self.qmui_borderLayer.lineWidth = self.qmui_borderWidth; + self.qmui_borderLayer.strokeColor = self.qmui_borderColor.CGColor; + self.qmui_borderLayer.lineDashPhase = self.qmui_dashPhase; + self.qmui_borderLayer.lineDashPattern = self.qmui_dashPattern; + self.qmui_borderLayer.hidden = NO; +} + +static char kAssociatedObjectKey_borderLocation; +- (void)setQmui_borderLocation:(QMUIViewBorderLocation)qmui_borderLocation { + BOOL valueChanged = self.qmui_borderLocation != qmui_borderLocation; + objc_setAssociatedObject(self, &kAssociatedObjectKey_borderLocation, @(qmui_borderLocation), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self _qmuibd_createBorderLayerIfNeeded]; + if (valueChanged && self.qmui_borderLayer && !self.qmui_borderLayer.hidden) { + [self setNeedsLayout]; + } +} + +- (QMUIViewBorderLocation)qmui_borderLocation { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_borderLocation)) unsignedIntegerValue]; +} + +static char kAssociatedObjectKey_borderPosition; +- (void)setQmui_borderPosition:(QMUIViewBorderPosition)qmui_borderPosition { + BOOL valueChanged = self.qmui_borderPosition != qmui_borderPosition; + objc_setAssociatedObject(self, &kAssociatedObjectKey_borderPosition, @(qmui_borderPosition), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self _qmuibd_createBorderLayerIfNeeded]; + if (valueChanged && self.qmui_borderLayer && !self.qmui_borderLayer.hidden) { + [self setNeedsLayout]; + } +} + +- (QMUIViewBorderPosition)qmui_borderPosition { + return (QMUIViewBorderPosition)[objc_getAssociatedObject(self, &kAssociatedObjectKey_borderPosition) unsignedIntegerValue]; +} + +static char kAssociatedObjectKey_borderWidth; +- (void)setQmui_borderWidth:(CGFloat)qmui_borderWidth { + BOOL valueChanged = self.qmui_borderWidth != qmui_borderWidth; + objc_setAssociatedObject(self, &kAssociatedObjectKey_borderWidth, @(qmui_borderWidth), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self _qmuibd_createBorderLayerIfNeeded]; + if (valueChanged && self.qmui_borderLayer && !self.qmui_borderLayer.hidden) { + [self setNeedsLayout]; + } +} + +- (CGFloat)qmui_borderWidth { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_borderWidth)) qmui_CGFloatValue]; +} + +static char kAssociatedObjectKey_borderInsets; +- (void)setQmui_borderInsets:(UIEdgeInsets)qmui_borderInsets { + BOOL valueChanged = !UIEdgeInsetsEqualToEdgeInsets(self.qmui_borderInsets, qmui_borderInsets); + objc_setAssociatedObject(self, &kAssociatedObjectKey_borderInsets, @(qmui_borderInsets), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self _qmuibd_createBorderLayerIfNeeded]; + if (valueChanged && self.qmui_borderLayer && !self.qmui_borderLayer.hidden) { + [self setNeedsLayout]; + } +} + +- (UIEdgeInsets)qmui_borderInsets { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_borderInsets)) UIEdgeInsetsValue]; +} + +static char kAssociatedObjectKey_borderColor; +- (void)setQmui_borderColor:(UIColor *)qmui_borderColor { + objc_setAssociatedObject(self, &kAssociatedObjectKey_borderColor, qmui_borderColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self _qmuibd_createBorderLayerIfNeeded]; + if (self.qmui_borderLayer && !self.qmui_borderLayer.hidden) { + [self setNeedsLayout]; + } +} + +- (UIColor *)qmui_borderColor { + return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_borderColor); +} + +static char kAssociatedObjectKey_dashPhase; +- (void)setQmui_dashPhase:(CGFloat)qmui_dashPhase { + BOOL valueChanged = self.qmui_dashPhase != qmui_dashPhase; + objc_setAssociatedObject(self, &kAssociatedObjectKey_dashPhase, @(qmui_dashPhase), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self _qmuibd_createBorderLayerIfNeeded]; + if (valueChanged && self.qmui_borderLayer && !self.qmui_borderLayer.hidden) { + [self setNeedsLayout]; + } +} + +- (CGFloat)qmui_dashPhase { + return [(NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_dashPhase) qmui_CGFloatValue]; +} + +static char kAssociatedObjectKey_dashPattern; +- (void)setQmui_dashPattern:(NSArray *)qmui_dashPattern { + BOOL valueChanged = [self.qmui_dashPattern isEqualToArray:qmui_dashPattern]; + objc_setAssociatedObject(self, &kAssociatedObjectKey_dashPattern, qmui_dashPattern, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self _qmuibd_createBorderLayerIfNeeded]; + if (valueChanged && self.qmui_borderLayer && !self.qmui_borderLayer.hidden) { + [self setNeedsLayout]; + } +} + +- (NSArray *)qmui_dashPattern { + return (NSArray *)objc_getAssociatedObject(self, &kAssociatedObjectKey_dashPattern); +} + +@end + +@implementation QMUIBorderLayer + +- (void)layoutSublayers { + [super layoutSublayers]; + if (!self._qmuibd_targetBorderView) return; + + UIView *view = self._qmuibd_targetBorderView; + CGFloat borderWidth = self.lineWidth; + UIEdgeInsets borderInsets = view.qmui_borderInsets; + + UIBezierPath *path = [UIBezierPath bezierPath];; + + CGFloat (^adjustsLocation)(CGFloat, CGFloat, CGFloat) = ^CGFloat(CGFloat inside, CGFloat center, CGFloat outside) { + return view.qmui_borderLocation == QMUIViewBorderLocationInside ? inside : (view.qmui_borderLocation == QMUIViewBorderLocationCenter ? center : outside); + }; + + CGFloat lineOffset = adjustsLocation(borderWidth / 2.0, 0, -borderWidth / 2.0); // 为了像素对齐而做的偏移 + CGFloat lineCapOffset = adjustsLocation(0, borderWidth / 2.0, borderWidth); // 两条相邻的边框连接的位置 + CGFloat verticalInset = borderInsets.top - borderInsets.bottom; + + BOOL shouldShowTopBorder = (view.qmui_borderPosition & QMUIViewBorderPositionTop) == QMUIViewBorderPositionTop; + BOOL shouldShowLeftBorder = (view.qmui_borderPosition & QMUIViewBorderPositionLeft) == QMUIViewBorderPositionLeft; + BOOL shouldShowBottomBorder = (view.qmui_borderPosition & QMUIViewBorderPositionBottom) == QMUIViewBorderPositionBottom; + BOOL shouldShowRightBorder = (view.qmui_borderPosition & QMUIViewBorderPositionRight) == QMUIViewBorderPositionRight; + + NSDictionary *> *points = @{ + @"toppath": @[ + [NSValue valueWithCGPoint:CGPointMake( + (shouldShowLeftBorder ? (-lineCapOffset + verticalInset) : 0) + borderInsets.left, + lineOffset + verticalInset + )], + [NSValue valueWithCGPoint:CGPointMake( + CGRectGetWidth(self.bounds) + (shouldShowRightBorder ? (lineCapOffset - verticalInset) : 0) - borderInsets.right, + lineOffset + verticalInset + )], + ], + @"leftpath": @[ + [NSValue valueWithCGPoint:CGPointMake( + lineOffset + verticalInset, + CGRectGetHeight(self.bounds) + (shouldShowBottomBorder ? lineCapOffset - verticalInset : 0) - borderInsets.left + )], + [NSValue valueWithCGPoint:CGPointMake( + lineOffset + verticalInset, + (shouldShowTopBorder ? -lineCapOffset + verticalInset : 0) + borderInsets.right + )], + ], + @"bottompath": @[ + [NSValue valueWithCGPoint:CGPointMake( + CGRectGetWidth(self.bounds) + (shouldShowRightBorder ? (lineCapOffset - verticalInset) : 0) - borderInsets.left, + CGRectGetHeight(self.bounds) - lineOffset - verticalInset + )], + [NSValue valueWithCGPoint:CGPointMake( + (shouldShowLeftBorder ? (-lineCapOffset + verticalInset) : 0) + borderInsets.right, + CGRectGetHeight(self.bounds) - lineOffset - verticalInset + )], + ], + @"rightpath": @[ + [NSValue valueWithCGPoint:CGPointMake( + CGRectGetWidth(self.bounds) - lineOffset - verticalInset, + (shouldShowTopBorder ? -lineCapOffset + verticalInset : 0) + borderInsets.left + )], + [NSValue valueWithCGPoint:CGPointMake( + CGRectGetWidth(self.bounds) - lineOffset - verticalInset, + CGRectGetHeight(self.bounds) + (shouldShowBottomBorder ? lineCapOffset - verticalInset : 0) - borderInsets.right + )], + ], + }; + + UIBezierPath *topPath = [UIBezierPath bezierPath]; + UIBezierPath *leftPath = [UIBezierPath bezierPath]; + UIBezierPath *bottomPath = [UIBezierPath bezierPath]; + UIBezierPath *rightPath = [UIBezierPath bezierPath]; + + if (view.layer.qmui_originCornerRadius > 0) { + + CGFloat cornerRadius = view.layer.qmui_originCornerRadius; + CGFloat radius = cornerRadius - lineOffset; + + if (view.layer.qmui_maskedCorners) { + if ((view.layer.qmui_maskedCorners & QMUILayerMinXMinYCorner) == QMUILayerMinXMinYCorner) { + [topPath addArcWithCenter:CGPointMake(cornerRadius + borderInsets.left + (shouldShowLeftBorder ? verticalInset : 0), cornerRadius + verticalInset) radius:radius startAngle:1.25 * M_PI endAngle:1.5 * M_PI clockwise:YES]; + [topPath addLineToPoint:CGPointMake(CGRectGetWidth(self.bounds) - cornerRadius - borderInsets.right - (shouldShowRightBorder ? verticalInset : 0), lineOffset + verticalInset)]; + [leftPath addArcWithCenter:CGPointMake(cornerRadius + verticalInset, cornerRadius + borderInsets.right + (shouldShowTopBorder ? verticalInset : 0)) radius:radius startAngle:-0.75 * M_PI endAngle:-1 * M_PI clockwise:NO]; + [leftPath addLineToPoint:CGPointMake(lineOffset + verticalInset, CGRectGetHeight(self.bounds) - cornerRadius - borderInsets.left - (shouldShowBottomBorder ? verticalInset : 0))]; + } else { + [topPath moveToPoint:points[@"toppath"][0].CGPointValue]; + [topPath addLineToPoint:CGPointMake(points[@"toppath"][1].CGPointValue.x - cornerRadius, points[@"toppath"][1].CGPointValue.y)]; + [leftPath moveToPoint:CGPointMake(points[@"leftpath"][0].CGPointValue.x, points[@"leftpath"][0].CGPointValue.y - cornerRadius)]; + [leftPath addLineToPoint:points[@"leftpath"][1].CGPointValue]; + } + if ((view.layer.qmui_maskedCorners & QMUILayerMinXMaxYCorner) == QMUILayerMinXMaxYCorner) { + [leftPath addArcWithCenter:CGPointMake(cornerRadius + verticalInset, CGRectGetHeight(self.bounds) - cornerRadius - borderInsets.left - (shouldShowBottomBorder ? verticalInset : 0)) radius:radius startAngle:-1 * M_PI endAngle:-1.25 * M_PI clockwise:NO]; + [bottomPath addArcWithCenter:CGPointMake(cornerRadius + borderInsets.right + (shouldShowLeftBorder ? verticalInset : 0), CGRectGetHeight(self.bounds) - cornerRadius - verticalInset) radius:radius startAngle:-1.25 * M_PI endAngle:-1.5 * M_PI clockwise:NO]; + [bottomPath addLineToPoint:CGPointMake(CGRectGetWidth(self.bounds) - cornerRadius - borderInsets.left - (shouldShowRightBorder ? verticalInset : 0), CGRectGetHeight(self.bounds) - lineOffset - verticalInset)]; + } else { + [leftPath moveToPoint:points[@"leftpath"][0].CGPointValue]; + [leftPath addLineToPoint:CGPointMake(points[@"leftpath"][0].CGPointValue.x, points[@"leftpath"][0].CGPointValue.y - cornerRadius)]; + [bottomPath moveToPoint:points[@"bottompath"][1].CGPointValue]; + [bottomPath addLineToPoint:CGPointMake(points[@"bottompath"][0].CGPointValue.x - cornerRadius, points[@"bottompath"][0].CGPointValue.y)]; + } + if ((view.layer.qmui_maskedCorners & QMUILayerMaxXMaxYCorner) == QMUILayerMaxXMaxYCorner) { + [bottomPath addArcWithCenter:CGPointMake(CGRectGetWidth(self.bounds) - cornerRadius - borderInsets.left - (shouldShowRightBorder ? verticalInset : 0), CGRectGetHeight(self.bounds) - cornerRadius - verticalInset) radius:radius startAngle:-1.5 * M_PI endAngle:-1.75 * M_PI clockwise:NO]; + [rightPath addArcWithCenter:CGPointMake(CGRectGetWidth(self.bounds) - cornerRadius - verticalInset, CGRectGetHeight(self.bounds) - cornerRadius - borderInsets.right - (shouldShowBottomBorder ? verticalInset : 0)) radius:radius startAngle:-1.75 * M_PI endAngle:-2 * M_PI clockwise:NO]; + [rightPath addLineToPoint:CGPointMake(CGRectGetWidth(self.bounds) - lineOffset - verticalInset, cornerRadius + borderInsets.left + (shouldShowTopBorder ? verticalInset : 0))]; + } else { + [bottomPath addLineToPoint:points[@"bottompath"][0].CGPointValue]; + [rightPath moveToPoint:points[@"rightpath"][1].CGPointValue]; + [rightPath addLineToPoint:CGPointMake(points[@"rightpath"][0].CGPointValue.x, points[@"rightpath"][0].CGPointValue.y + cornerRadius)]; + } + if ((view.layer.qmui_maskedCorners & QMUILayerMaxXMinYCorner) == QMUILayerMaxXMinYCorner) { + [rightPath addArcWithCenter:CGPointMake(CGRectGetWidth(self.bounds) - cornerRadius - verticalInset, cornerRadius + borderInsets.left + (shouldShowTopBorder ? verticalInset : 0)) radius:radius startAngle:0 * M_PI endAngle:-0.25 * M_PI clockwise:NO]; + [topPath addArcWithCenter:CGPointMake(CGRectGetWidth(self.bounds) - cornerRadius - borderInsets.right - (shouldShowRightBorder ? verticalInset : 0), cornerRadius + verticalInset) radius:radius startAngle:1.5 * M_PI endAngle:1.75 * M_PI clockwise:YES]; + } else { + [rightPath addLineToPoint:points[@"rightpath"][0].CGPointValue]; + [topPath addLineToPoint:points[@"toppath"][1].CGPointValue]; + } + } else { + [topPath addArcWithCenter:CGPointMake(cornerRadius, cornerRadius) radius:radius startAngle:1.25 * M_PI endAngle:1.5 * M_PI clockwise:YES]; + [topPath addLineToPoint:CGPointMake(CGRectGetWidth(self.bounds) - cornerRadius, lineOffset)]; + [topPath addArcWithCenter:CGPointMake(CGRectGetWidth(self.bounds) - cornerRadius, cornerRadius) radius:radius startAngle:1.5 * M_PI endAngle:1.75 * M_PI clockwise:YES]; + + [leftPath addArcWithCenter:CGPointMake(cornerRadius, cornerRadius) radius:radius startAngle:-0.75 * M_PI endAngle:-1 * M_PI clockwise:NO]; + [leftPath addLineToPoint:CGPointMake(lineOffset, CGRectGetHeight(self.bounds) - cornerRadius)]; + [leftPath addArcWithCenter:CGPointMake(cornerRadius, CGRectGetHeight(self.bounds) - cornerRadius) radius:radius startAngle:-1 * M_PI endAngle:-1.25 * M_PI clockwise:NO]; + + [bottomPath addArcWithCenter:CGPointMake(cornerRadius, CGRectGetHeight(self.bounds) - cornerRadius) radius:radius startAngle:-1.25 * M_PI endAngle:-1.5 * M_PI clockwise:NO]; + [bottomPath addLineToPoint:CGPointMake(CGRectGetWidth(self.bounds) - cornerRadius, CGRectGetHeight(self.bounds) - lineOffset)]; + [bottomPath addArcWithCenter:CGPointMake(CGRectGetHeight(self.bounds) - cornerRadius, CGRectGetHeight(self.bounds) - cornerRadius) radius:radius startAngle:-1.5 * M_PI endAngle:-1.75 * M_PI clockwise:NO]; + + [rightPath addArcWithCenter:CGPointMake(CGRectGetWidth(self.bounds) - cornerRadius, CGRectGetHeight(self.bounds) - cornerRadius) radius:radius startAngle:-1.75 * M_PI endAngle:-2 * M_PI clockwise:NO]; + [rightPath addLineToPoint:CGPointMake(CGRectGetWidth(self.bounds) - lineOffset, cornerRadius)]; + [rightPath addArcWithCenter:CGPointMake(CGRectGetWidth(self.bounds) - cornerRadius, cornerRadius) radius:radius startAngle:0 * M_PI endAngle:-0.25 * M_PI clockwise:NO]; + } + + } else { + + [topPath moveToPoint:points[@"toppath"][0].CGPointValue]; // 左上角 + [topPath addLineToPoint:points[@"toppath"][1].CGPointValue]; // 右上角 + + [leftPath moveToPoint:points[@"leftpath"][0].CGPointValue]; // 左下角 + [leftPath addLineToPoint:points[@"leftpath"][1].CGPointValue]; // 左上角 + + [bottomPath moveToPoint:points[@"bottompath"][0].CGPointValue]; // 右下角 + [bottomPath addLineToPoint:points[@"bottompath"][1].CGPointValue]; // 左下角 + + [rightPath moveToPoint:points[@"rightpath"][0].CGPointValue]; // 右上角 + [rightPath addLineToPoint:points[@"rightpath"][1].CGPointValue]; // 右下角 + } + + if (shouldShowTopBorder && ![topPath isEmpty]) { + [path appendPath:topPath]; + } + if (shouldShowLeftBorder && ![leftPath isEmpty]) { + [path appendPath:leftPath]; + } + if (shouldShowBottomBorder && ![bottomPath isEmpty]) { + [path appendPath:bottomPath]; + } + if (shouldShowRightBorder && ![rightPath isEmpty]) { + [path appendPath:rightPath]; + } + + self.path = path.CGPath; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIViewController+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIViewController+QMUI.h index 6db7f9a0..51848248 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIViewController+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIViewController+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIViewController+QMUI.h @@ -28,18 +28,24 @@ extern NSString *const QMUIPrecedingAppSizeUserInfoKey; extern NSString *const QMUIFollowingAppSizeUserInfoKey; typedef NS_OPTIONS(NSUInteger, QMUIViewControllerVisibleState) { - QMUIViewControllerUnknow = 1 << 0, /// 初始化完成但尚未触发 viewDidLoad - QMUIViewControllerViewDidLoad = 1 << 1, /// 触发了 viewDidLoad - QMUIViewControllerWillAppear = 1 << 2, /// 触发了 viewWillAppear - QMUIViewControllerDidAppear = 1 << 3, /// 触发了 viewDidAppear - QMUIViewControllerWillDisappear = 1 << 4, /// 触发了 viewWillDisappear - QMUIViewControllerDidDisappear = 1 << 5, /// 触发了 viewDidDisappear + QMUIViewControllerUnknow = 1 << 0, // 初始化完成但尚未触发 viewDidLoad + QMUIViewControllerViewDidLoad = 1 << 1, // 触发了 viewDidLoad + QMUIViewControllerWillAppear = 1 << 2, // 触发了 viewWillAppear + QMUIViewControllerDidAppear = 1 << 3, // 触发了 viewDidAppear + QMUIViewControllerWillDisappear = 1 << 4, // 触发了 viewWillDisappear + QMUIViewControllerDidDisappear = 1 << 5, // 触发了 viewDidDisappear QMUIViewControllerVisible = QMUIViewControllerWillAppear | QMUIViewControllerDidAppear,// 表示是否处于可视范围,判断时请用 & 运算,例如 qmui_visibleState & QMUIViewControllerVisible }; @interface UIViewController (QMUI) +/// 当前 UIViewController.class 是否为系统默认的几个 container viewController(也即 UINavigationController、UITabBarController、UISplitViewController)。 +@property(class, nonatomic, assign, readonly) BOOL qmui_isSystemContainerViewController; + +/// 当前 UIViewController 是否为系统默认的几个 container viewController(也即 UINavigationController、UITabBarController、UISplitViewController)。 +@property(nonatomic, assign, readonly) BOOL qmui_isSystemContainerViewController; + /** 获取和自身处于同一个UINavigationController里的上一个UIViewController */ @property(nullable, nonatomic, weak, readonly) UIViewController *qmui_previousViewController; @@ -66,6 +72,11 @@ typedef NS_OPTIONS(NSUInteger, QMUIViewControllerVisibleState) { */ - (BOOL)qmui_isViewLoadedAndVisible; +/** + 判断当前 viewController 是否为传入的 viewController 本身,或是其“子控制器” (childViewController)、孙子控制器(即 childViewController 的 childViewController ...) + */ +- (BOOL)qmui_isDescendantOfViewController:(UIViewController *)viewController; + /** 获取当前 viewController 所处的的生命周期阶段(也即 viewDidLoad/viewWillApear/viewDidAppear/viewWillDisappear/viewDidDisappear) */ @@ -97,6 +108,20 @@ typedef NS_OPTIONS(NSUInteger, QMUIViewControllerVisibleState) { */ @property(nonatomic, assign, readonly) CGFloat qmui_tabBarSpacingInViewCoordinator; +/// 提供一个 block 可以方便地控制是否要隐藏状态栏,适用于无法重写父类方法的场景。默认不实现这个 block 则不干预显隐。 +@property(nullable, nonatomic, copy) BOOL (^qmui_prefersStatusBarHiddenBlock)(void); + +/// 提供一个 block 可以方便地控制状态栏样式,适用于无法重写父类方法的场景。默认不实现这个 block 则不干预样式。 +/// @note iOS 13 及以后,自己显示的 UIWindow 无法盖住状态栏了,但 iOS 12 及以前的系统,以 UIWindow 显示的浮层是可以盖住状态栏的,请知悉。 +/// @note 对于 QMUISearchController,这个 block 的返回值将会用于控制搜索状态下的状态栏样式。 +@property(nullable, nonatomic, copy) UIStatusBarStyle (^qmui_preferredStatusBarStyleBlock)(void); + +/// 提供一个 block 可以方便地控制状态栏动画,适用于无法重写父类方法的场景。默认不实现这个 block 则不干预动画。 +@property(nullable, nonatomic, copy) UIStatusBarAnimation (^qmui_preferredStatusBarUpdateAnimationBlock)(void); + +/// 提供一个 block 可以方便地控制全面屏设备屏幕底部的 Home Indicator 的显隐,适用于无法重写父类方法的场景。默认不实现这个 block 则不干预显隐。 +@property(nullable, nonatomic, copy) BOOL (^qmui_prefersHomeIndicatorAutoHiddenBlock)(void); + /** 获取当前 viewController 的 statusBar 显隐状态,与系统 prefersStatusBarHidden 的区别在于,系统的方法在对 containerViewController(例如 UITabBarController、UINavigationController 等)调用时,返回的是 containerViewController 自身的 prefersStatusBarHidden 的值,但真正决定 statusBar 显隐的是该 containerViewController 的 childViewControllerForStatusBarHidden 的 prefersStatusBarHidden 的值,所以只有用 qmui_prefersStatusBarHidden 才能拿到真正的值。 */ @@ -107,6 +132,12 @@ typedef NS_OPTIONS(NSUInteger, QMUIViewControllerVisibleState) { */ @property(nonatomic, assign, readonly) UIStatusBarStyle qmui_preferredStatusBarStyle; +/** + 判断当前 viewController 是否具备显示 LargeTitle 的条件 + @warning 需要 viewController 在 navigationController 栈内才能正确判断 + */ +@property(nonatomic, assign, readonly) BOOL qmui_prefersLargeTitleDisplayed; + @end /** @@ -116,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 就会被自动执行。 @@ -133,13 +165,6 @@ typedef NS_OPTIONS(NSUInteger, QMUIViewControllerVisibleState) { - (BOOL)qmui_hasOverrideUIKitMethod:(_Nonnull SEL)selector; @end -@interface UIViewController (RotateDeviceOrientation) - -/// 在配置表 AutomaticallyRotateDeviceOrientation 功能开启的情况下,QMUI 会自动判断当前的 UIViewController 是否具备强制旋转设备方向的权利,而如果 QMUI 判断结果为没权利但你又希望当前的 UIViewController 具备这个权利,则可以重写该方法并返回 YES。 -/// 默认返回 NO,也即交给 QMUI 自动判断。 -- (BOOL)qmui_shouldForceRotateDeviceOrientation; -@end - @interface UIViewController (QMUINavigationController) /// 判断当前 viewController 是否处于手势返回中,仅对当前手势返回涉及到的前后两个 viewController 有效 @@ -154,6 +179,15 @@ typedef NS_OPTIONS(NSUInteger, QMUIViewControllerVisibleState) { /// 当前 viewController 是否是手势返回中,背后的那个界面 @property(nonatomic, assign) BOOL qmui_willAppearByInteractivePopGestureRecognizer; + + +/// 可用于对 View 执行一些操作, 如果此时处于转场过渡中,这些操作会跟随转场进度以动画的形式展示过程 +/// @param animation 要执行的操作 +/// @param completion 转场完成或取消后的回调 +/// @note 如果处于非转场过程中,也会执行 animation ,随后执行 completion,业务无需关心是否处于转场过程中。 +- (void)qmui_animateAlongsideTransition:(void (^ __nullable)(id context))animation + completion:(void (^ __nullable)(id context))completion; + @end @interface QMUIHelper (ViewController) diff --git a/QMUI/QMUIKit/UIKitExtensions/UIViewController+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIViewController+QMUI.m index ca122d89..de9fbbcf 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIViewController+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIViewController+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIViewController+QMUI.m @@ -25,16 +25,13 @@ NSString *const QMUIPrecedingAppSizeUserInfoKey = @"QMUIPrecedingAppSizeUserInfoKey"; NSString *const QMUIFollowingAppSizeUserInfoKey = @"QMUIFollowingAppSizeUserInfoKey"; -NSString *const QMUITabBarStyleChangedNotification = @"QMUITabBarStyleChangedNotification"; - -@interface UIViewController () - -@property(nonatomic, strong) UINavigationBar *transitionNavigationBar;// by molice 对应 UIViewController (NavigationBarTransition) 里的 transitionNavigationBar,为了让这个属性在这里可以被访问到,有点 hack,具体请查看 https://github.com/Tencent/QMUI_iOS/issues/268 -@end - @implementation UIViewController (QMUI) QMUISynthesizeIdCopyProperty(qmui_visibleStateDidChangeBlock, setQmui_visibleStateDidChangeBlock) +QMUISynthesizeIdCopyProperty(qmui_prefersStatusBarHiddenBlock, setQmui_prefersStatusBarHiddenBlock) +QMUISynthesizeIdCopyProperty(qmui_preferredStatusBarStyleBlock, setQmui_preferredStatusBarStyleBlock) +QMUISynthesizeIdCopyProperty(qmui_preferredStatusBarUpdateAnimationBlock, setQmui_preferredStatusBarUpdateAnimationBlock) +QMUISynthesizeIdCopyProperty(qmui_prefersHomeIndicatorAutoHiddenBlock, setQmui_prefersHomeIndicatorAutoHiddenBlock) + (void)load { static dispatch_once_t onceToken; @@ -65,14 +62,11 @@ + (void)load { OverrideImplementation([UIViewController class], @selector(viewWillTransitionToSize:withTransitionCoordinator:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIViewController *selfObject, CGSize size, id coordinator) { - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (selfObject == UIApplication.sharedApplication.delegate.window.rootViewController) { - CGSize originalSize = selfObject.view.frame.size; - BOOL sizeChanged = !CGSizeEqualToSize(originalSize, size); - if (sizeChanged) { - [[NSNotificationCenter defaultCenter] postNotificationName:QMUIAppSizeWillChangeNotification object:nil userInfo:@{QMUIPrecedingAppSizeUserInfoKey: @(originalSize), QMUIFollowingAppSizeUserInfoKey: @(size)}]; - } + if (selfObject == UIApplication.sharedApplication.delegate.window.rootViewController) { + CGSize originalSize = selfObject.view.frame.size; + BOOL sizeChanged = !CGSizeEqualToSize(originalSize, size); + if (sizeChanged) { + [[NSNotificationCenter defaultCenter] postNotificationName:QMUIAppSizeWillChangeNotification object:nil userInfo:@{QMUIPrecedingAppSizeUserInfoKey: @(originalSize), QMUIFollowingAppSizeUserInfoKey: @(size)}]; } } @@ -83,33 +77,131 @@ + (void)load { }; }); - // 修复 iOS 11 scrollView 无法自动适配不透明的 tabBar,导致底部 inset 错误的问题 + // 修复 iOS 11 及以后,UIScrollView 无法自动适配不透明的 tabBar,导致底部 inset 错误的问题 // https://github.com/Tencent/QMUI_iOS/issues/218 - if (@available(iOS 11, *)) { - ExtendImplementationOfNonVoidMethodWithTwoArguments([UIViewController class], @selector(initWithNibName:bundle:), NSString *, NSBundle *, UIViewController *, ^UIViewController *(UIViewController *selfObject, NSString *nibNameOrNil, NSBundle *nibBundleOrNil, UIViewController *originReturnValue) { - BOOL isContainerViewController = [selfObject isKindOfClass:[UINavigationController class]] || [selfObject isKindOfClass:[UITabBarController class]] || [selfObject isKindOfClass:[UISplitViewController class]]; - if (!isContainerViewController) { - [[NSNotificationCenter defaultCenter] addObserver:selfObject selector:@selector(adjustsAdditionalSafeAreaInsetsForOpaqueTabBarWithNotification:) name:QMUITabBarStyleChangedNotification object:nil]; - } - return originReturnValue; + if (!QMUICMIActivated || ShouldFixTabBarSafeAreaInsetsBug) { + // -[UIViewController _setContentOverlayInsets:andLeftMargin:rightMargin:] + OverrideImplementation([UIViewController class], NSSelectorFromString([NSString stringWithFormat:@"_%@:%@:%@:",@"setContentOverlayInsets", @"andLeftMargin", @"rightMargin"]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIViewController *selfObject, UIEdgeInsets insets, CGFloat leftMargin, CGFloat rightMargin) { + + UITabBarController *tabBarController = selfObject.tabBarController; + UITabBar *tabBar = tabBarController.tabBar; + if (tabBarController + && tabBar + && selfObject.navigationController.parentViewController == tabBarController + && selfObject.parentViewController == selfObject.navigationController // 过滤掉那些自己添加的 childViewController 的情况 + && !tabBar.hidden + && !selfObject.hidesBottomBarWhenPushed + && selfObject.isViewLoaded) { + CGRect viewRectInTabBarController = [selfObject.view convertRect:selfObject.view.bounds toView:tabBarController.view]; + + // 发现在 iOS 13.3 及以下,在 extendedLayoutIncludesOpaqueBars = YES 的情况下,理论上任何时候 vc.view 都应该撑满整个 tabBarController.view,但从没带 tabBar 的界面 pop 到带 tabBar 的界面过程中,navController.view.height 会被改得小一点,导致 safeAreaInsets.bottom 出现错误的中间值,引发 UIScrollView.contentInset 的错误变化,后续就算 contentInset 恢复正确,contentOffset 也无法恢复,所以这里直接过滤掉中间的错误值 + // (但无法保证每个场景下这样的值都是错的,或许某些少见的场景里,navController.view.height 就是不会铺满整个 tabBarController.view.height 呢?) + // https://github.com/Tencent/QMUI_iOS/issues/934 + if (@available(iOS 13.4, *)) { + } else { + if (( + (!tabBar.translucent && selfObject.extendedLayoutIncludesOpaqueBars) + || tabBar.translucent + ) + && selfObject.edgesForExtendedLayout & UIRectEdgeBottom + && !CGFloatEqualToFloat(CGRectGetHeight(viewRectInTabBarController), CGRectGetHeight(tabBarController.view.bounds))) { + return; + } + } + + // pop 转场动画过程中有些时候 tabBar 尚未被加到 view 层级树里,所以这里做个判断,避免出现 convertRect 警告 + CGRect barRectInTabBarController = tabBar.window ? [tabBar convertRect:tabBar.bounds toView:tabBarController.view] : tabBar.frame; + CGFloat correctInsetBottom = MAX(CGRectGetMaxY(viewRectInTabBarController) - CGRectGetMinY(barRectInTabBarController), 0); + insets.bottom = correctInsetBottom; + } + + // call super + void (*originSelectorIMP)(id, SEL, UIEdgeInsets, CGFloat, CGFloat); + originSelectorIMP = (void (*)(id, SEL, UIEdgeInsets, CGFloat, CGFloat))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, insets, leftMargin, rightMargin); + }; }); } + + // iOS 11 及以后不 override prefersStatusBarHidden 而是通过私有方法来实现,是因为系统会先通过 +[UIViewController doesOverrideViewControllerMethod:inBaseClass:] 方法来判断当前的 UIViewController 有没有重写 prefersStatusBarHidden 方法,有的话才会去调用 prefersStatusBarHidden,而如果我们用 swizzle 的方式去重写 prefersStatusBarHidden,系统依然会认为你没有重写该方法,于是不会调用,于是 block 无效。对于 iOS 10 及以前的系统没有这种逻辑,所以没问题。 + // 特别的,只有 hidden 操作有这种逻辑,而 style、animation 等操作不管在哪个 iOS 版本里都是没有这种逻辑的 + OverrideImplementation([UIViewController class], NSSelectorFromString(@"_preferredStatusBarVisibility"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^NSInteger(UIViewController *selfObject) { + // 为了保证重写 prefersStatusBarHidden 的优先级比 block 高,这里要判断一下 qmui_hasOverrideUIKitMethod 的值 + if (![selfObject qmui_hasOverrideUIKitMethod:@selector(prefersStatusBarHidden)] && selfObject.qmui_prefersStatusBarHiddenBlock) { + return selfObject.qmui_prefersStatusBarHiddenBlock() ? 1 : 2;// 系统返回的 1 表示隐藏,2 表示显示,0 不清楚含义 + } + + // call super + NSInteger (*originSelectorIMP)(id, SEL); + originSelectorIMP = (NSInteger (*)(id, SEL))originalIMPProvider(); + NSInteger result = originSelectorIMP(selfObject, originCMD); + return result; + }; + }); + + OverrideImplementation([UIViewController class], @selector(preferredStatusBarStyle), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIStatusBarStyle(UIViewController *selfObject) { + if (selfObject.qmui_preferredStatusBarStyleBlock) { + return selfObject.qmui_preferredStatusBarStyleBlock(); + } + + // call super + UIStatusBarStyle (*originSelectorIMP)(id, SEL); + originSelectorIMP = (UIStatusBarStyle (*)(id, SEL))originalIMPProvider(); + UIStatusBarStyle result = originSelectorIMP(selfObject, originCMD); + return result; + }; + }); + + OverrideImplementation([UIViewController class], @selector(preferredStatusBarUpdateAnimation), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIStatusBarAnimation(UIViewController *selfObject) { + if (selfObject.qmui_preferredStatusBarUpdateAnimationBlock) { + return selfObject.qmui_preferredStatusBarUpdateAnimationBlock(); + } + + // call super + UIStatusBarAnimation (*originSelectorIMP)(id, SEL); + originSelectorIMP = (UIStatusBarAnimation (*)(id, SEL))originalIMPProvider(); + UIStatusBarAnimation result = originSelectorIMP(selfObject, originCMD); + return result; + }; + }); + + OverrideImplementation([UIViewController class], @selector(prefersHomeIndicatorAutoHidden), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^BOOL(UIViewController *selfObject) { + if (selfObject.qmui_prefersHomeIndicatorAutoHiddenBlock) { + return selfObject.qmui_prefersHomeIndicatorAutoHiddenBlock(); + } + + // call super + BOOL (*originSelectorIMP)(id, SEL); + originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider(); + BOOL result = originSelectorIMP(selfObject, originCMD); + return result; + }; + }); }); } - (NSString *)qmuivc_description { - NSString *result = [NSString stringWithFormat:@"%@\nsuperclass:\t\t\t\t%@\ntitle:\t\t\t\t\t%@\nview:\t\t\t\t\t%@", [self qmuivc_description], NSStringFromClass(self.superclass), self.title, [self isViewLoaded] ? self.view : nil]; + if (![NSThread isMainThread]) { + return [self qmuivc_description]; + } + + NSString *result = [NSString stringWithFormat:@"%@; superclass: %@; title: %@; view: %@", [self qmuivc_description], NSStringFromClass(self.superclass), self.title, [self isViewLoaded] ? self.view : nil]; if ([self isKindOfClass:[UINavigationController class]]) { UINavigationController *navController = (UINavigationController *)self; - NSString *navDescription = [NSString stringWithFormat:@"\nviewControllers(%@):\t\t%@\ntopViewController:\t\t%@\nvisibleViewController:\t%@", @(navController.viewControllers.count), [self descriptionWithViewControllers:navController.viewControllers], [navController.topViewController qmuivc_description], [navController.visibleViewController qmuivc_description]]; + NSString *navDescription = [NSString stringWithFormat:@"; viewControllers(%@): %@; topViewController: %@; visibleViewController: %@", @(navController.viewControllers.count), [self descriptionWithViewControllers:navController.viewControllers], [navController.topViewController qmuivc_description], [navController.visibleViewController qmuivc_description]]; result = [result stringByAppendingString:navDescription]; } else if ([self isKindOfClass:[UITabBarController class]]) { UITabBarController *tabBarController = (UITabBarController *)self; - NSString *tabBarDescription = [NSString stringWithFormat:@"\nviewControllers(%@):\t\t%@\nselectedViewController(%@):\t%@", @(tabBarController.viewControllers.count), [self descriptionWithViewControllers:tabBarController.viewControllers], @(tabBarController.selectedIndex), [tabBarController.selectedViewController qmuivc_description]]; + NSString *tabBarDescription = [NSString stringWithFormat:@"; viewControllers(%@): %@; selectedViewController(%@): %@", @(tabBarController.viewControllers.count), [self descriptionWithViewControllers:tabBarController.viewControllers], @(tabBarController.selectedIndex), [tabBarController.selectedViewController qmuivc_description]]; result = [result stringByAppendingString:tabBarDescription]; } @@ -118,14 +210,27 @@ - (NSString *)qmuivc_description { - (NSString *)descriptionWithViewControllers:(NSArray *)viewControllers { NSMutableString *string = [[NSMutableString alloc] init]; - [string appendString:@"(\n"]; + [string appendString:@"( "]; for (NSInteger i = 0, l = viewControllers.count; i < l; i++) { - [string appendFormat:@"\t\t\t\t\t\t\t[%@]%@%@\n", @(i), [viewControllers[i] qmuivc_description], i < l - 1 ? @"," : @""]; + [string appendFormat:@"[%@]%@%@", @(i), [viewControllers[i] qmuivc_description], i < l - 1 ? @"," : @""]; } - [string appendString:@"\t\t\t\t\t\t)"]; + [string appendString:@" )"]; return [string copy]; } ++ (BOOL)qmui_isSystemContainerViewController { + for (Class clz in @[UINavigationController.class, UITabBarController.class, UISplitViewController.class]) { + if ([self isSubclassOfClass:clz]) { + return YES; + } + } + return NO; +} + +- (BOOL)qmui_isSystemContainerViewController { + return self.class.qmui_isSystemContainerViewController; +} + static char kAssociatedObjectKey_visibleState; - (void)setQmui_visibleState:(QMUIViewControllerVisibleState)qmui_visibleState { BOOL valueChanged = self.qmui_visibleState != qmui_visibleState; @@ -139,43 +244,31 @@ - (QMUIViewControllerVisibleState)qmui_visibleState { return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_visibleState)) unsignedIntegerValue]; } -- (void)adjustsAdditionalSafeAreaInsetsForOpaqueTabBarWithNotification:(NSNotification *)notification { - if (@available(iOS 11, *)) { - - BOOL isCurrentTabBar = self.tabBarController && self.navigationController && self.navigationController.qmui_rootViewController == self && self.navigationController.parentViewController == self.tabBarController && (notification ? notification.object == self.tabBarController.tabBar : YES); - if (!isCurrentTabBar) { - return; - } - - UITabBar *tabBar = self.tabBarController.tabBar; +- (UIViewController *)qmui_previousViewController { + UIViewController *previousViewController = nil; + NSArray *viewControllers = self.navigationController.viewControllers; + NSUInteger index = [viewControllers indexOfObject:self]; + if (index != NSNotFound && index > 0) { + previousViewController = viewControllers[index - 1]; - // 这串判断条件来源于这个 issue:https://github.com/Tencent/QMUI_iOS/issues/218 - BOOL isOpaqueBarAndCanExtendedLayout = !tabBar.translucent && self.extendedLayoutIncludesOpaqueBars; - if (!isOpaqueBarAndCanExtendedLayout) { - return; + // 系统 UINavigationController 的 popToViewController、popToRootViewController、setViewControllers 三种 pop 的方式都有一个共同的特点,假如此时有3个及以上的 vc 例如 [A,B,C],从当前界面 pop 到非相邻的界面,例如C到A,执行完 pop 操作后立马访问 UINavigationController.viewControllers 预期应该得到 [A],实际上会得到 [C,A],过一会(nav.view layoutIfNeeded 之后)才变成正确的 [A]。同理,[A,B,C,D]时从 D pop 到 B,预期得到[A,B],实际得到[D,A,B],也即这种情况它总是会把当前界面塞到 viewControllers 数组里的第一个,这就导致这期间访问基于 viewControllers 数组实现的功能(例如 qmui_rootViewController、qmui_previousViewController),都可能出错,所以这里对上述情况做特殊保护。 + // 如果 pop 操作时只有2个vc,则没这种问题。 + if (self.navigationController.qmui_isPopping && self.navigationController.transitionCoordinator) { + id transitionCoordinator = self.navigationController.transitionCoordinator; + UIViewController *fromVc = [transitionCoordinator viewControllerForKey:UITransitionContextFromViewControllerKey]; + UIViewController *toVc = [transitionCoordinator viewControllerForKey:UITransitionContextToViewControllerKey]; + if (self == toVc && previousViewController == fromVc && index == 1) { + previousViewController = nil; + } } - - BOOL tabBarHidden = tabBar.hidden; - - // 这里直接用 CGRectGetHeight(tabBar.frame) 来计算理论上不准确,但因为系统有这个 bug(https://github.com/Tencent/QMUI_iOS/issues/217),所以暂时用 CGRectGetHeight(tabBar.frame) 来代替 - CGFloat correctSafeAreaInsetsBottom = tabBarHidden ? tabBar.safeAreaInsets.bottom : CGRectGetHeight(tabBar.frame); - CGFloat additionalSafeAreaInsetsBottom = correctSafeAreaInsetsBottom - tabBar.safeAreaInsets.bottom; - self.additionalSafeAreaInsets = UIEdgeInsetsSetBottom(self.additionalSafeAreaInsets, additionalSafeAreaInsetsBottom); } -} - -- (UIViewController *)qmui_previousViewController { - if (self.navigationController.viewControllers && self.navigationController.viewControllers.count > 1 && self.navigationController.topViewController == self) { - NSUInteger count = self.navigationController.viewControllers.count; - return (UIViewController *)[self.navigationController.viewControllers objectAtIndex:count - 2]; - } - return nil; + return previousViewController; } - (NSString *)qmui_previousViewControllerTitle { UIViewController *previousViewController = [self qmui_previousViewController]; if (previousViewController) { - return previousViewController.title; + return previousViewController.title ?: previousViewController.navigationItem.title; } return nil; } @@ -209,7 +302,7 @@ - (UIViewController *)qmui_visibleViewControllerIfExist { if ([self qmui_isViewLoadedAndVisible]) { return self; } else { - QMUILog(@"UIViewController (QMUI)", @"qmui_visibleViewControllerIfExist:,找不到可见的viewController。self = %@, self.view = %@, self.view.window = %@", self, [self isViewLoaded] ? self.view : nil, [self isViewLoaded] ? self.view.window : nil); + QMUILog(@"UIViewController (QMUI)", @"qmui_visibleViewControllerIfExist:,找不到可见的viewController。self = %@, self.view.window = %@", self, [self isViewLoaded] ? self.view.window : nil); return nil; } } @@ -328,6 +421,10 @@ - (CGFloat)qmui_tabBarSpacingInViewCoordinator { if (!self.tabBarController.tabBar || self.tabBarController.tabBar.hidden) { return 0; } + if (self.hidesBottomBarWhenPushed && self.navigationController.qmui_rootViewController != self) { + return 0; + } + CGRect tabBarFrame = CGRectIntersection(self.view.bounds, [self.view convertRect:self.tabBarController.tabBar.frame fromView:self.tabBarController.tabBar.superview]); // 两个 rect 如果不存在交集,CGRectIntersection 计算结果可能为非法的 rect,所以这里做个保护 @@ -353,6 +450,38 @@ - (UIStatusBarStyle)qmui_preferredStatusBarStyle { return self.preferredStatusBarStyle; } +- (BOOL)qmui_prefersLargeTitleDisplayed { + QMUIAssert(self.navigationController, @"UIViewController (QMUI)", @"%s 必现在 navigationController 栈内才能正确判断", __func__); + UINavigationBar *navigationBar = self.navigationController.navigationBar; + if (!navigationBar.prefersLargeTitles) { + return NO; + } + if (self.navigationItem.largeTitleDisplayMode == UINavigationItemLargeTitleDisplayModeAlways) { + return YES; + } else if (self.navigationItem.largeTitleDisplayMode == UINavigationItemLargeTitleDisplayModeNever) { + return NO; + } else if (self.navigationItem.largeTitleDisplayMode == UINavigationItemLargeTitleDisplayModeAutomatic) { + if (self.navigationController.viewControllers.firstObject == self) { + return YES; + } else { + UIViewController *previousViewController = self.navigationController.viewControllers[[self.navigationController.viewControllers indexOfObject:self] - 1]; + return previousViewController.qmui_prefersLargeTitleDisplayed == YES; + } + } + return NO; +} + +- (BOOL)qmui_isDescendantOfViewController:(UIViewController *)viewController { + UIViewController *parentViewController = self; + while (parentViewController) { + if (parentViewController == viewController) { + return YES; + } + parentViewController = parentViewController.parentViewController; + } + return NO; +} + @end @implementation UIViewController (Data) @@ -362,11 +491,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; + } + }; }); }); } @@ -418,62 +555,6 @@ - (BOOL)qmui_hasOverrideUIKitMethod:(SEL)selector { @end -@implementation UIViewController (RotateDeviceOrientation) - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - // 实现 AutomaticallyRotateDeviceOrientation 开关的功能 - ExtendImplementationOfVoidMethodWithSingleArgument([UIViewController class], @selector(viewWillAppear:), BOOL, ^(UIViewController *selfObject, BOOL animated) { - if (!AutomaticallyRotateDeviceOrientation) { - return; - } - - // 某些情况下的 UIViewController 不具备决定设备方向的权利,具体请看 https://github.com/Tencent/QMUI_iOS/issues/291 - if (![selfObject qmui_shouldForceRotateDeviceOrientation]) { - BOOL isRootViewController = [selfObject isViewLoaded] && selfObject.view.window.rootViewController == selfObject; - BOOL isChildViewController = [selfObject.tabBarController.viewControllers containsObject:selfObject] || [selfObject.navigationController.viewControllers containsObject:selfObject] || [selfObject.splitViewController.viewControllers containsObject:selfObject]; - BOOL hasRightsOfRotateDeviceOrientaion = isRootViewController || isChildViewController; - if (!hasRightsOfRotateDeviceOrientaion) { - return; - } - } - - - UIInterfaceOrientation statusBarOrientation = [[UIApplication sharedApplication] statusBarOrientation]; - UIDeviceOrientation deviceOrientationBeforeChangingByHelper = [QMUIHelper sharedInstance].orientationBeforeChangingByHelper; - BOOL shouldConsiderBeforeChanging = deviceOrientationBeforeChangingByHelper != UIDeviceOrientationUnknown; - UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation; - - // 虽然这两者的 unknow 值是相同的,但在启动 App 时可能只有其中一个是 unknown - if (statusBarOrientation == UIInterfaceOrientationUnknown || deviceOrientation == UIDeviceOrientationUnknown) return; - - // 如果当前设备方向和界面支持的方向不一致,则主动进行旋转 - UIDeviceOrientation deviceOrientationToRotate = [QMUIHelper interfaceOrientationMask:selfObject.supportedInterfaceOrientations containsDeviceOrientation:deviceOrientation] ? deviceOrientation : [QMUIHelper deviceOrientationWithInterfaceOrientationMask:selfObject.supportedInterfaceOrientations]; - - // 之前没用私有接口修改过,那就按最标准的方式去旋转 - if (!shouldConsiderBeforeChanging) { - if ([QMUIHelper rotateToDeviceOrientation:deviceOrientationToRotate]) { - [QMUIHelper sharedInstance].orientationBeforeChangingByHelper = deviceOrientation; - } else { - [QMUIHelper sharedInstance].orientationBeforeChangingByHelper = UIDeviceOrientationUnknown; - } - return; - } - - // 用私有接口修改过方向,但下一个界面和当前界面方向不相同,则要把修改前记录下来的那个设备方向考虑进来 - deviceOrientationToRotate = [QMUIHelper interfaceOrientationMask:selfObject.supportedInterfaceOrientations containsDeviceOrientation:deviceOrientationBeforeChangingByHelper] ? deviceOrientationBeforeChangingByHelper : [QMUIHelper deviceOrientationWithInterfaceOrientationMask:selfObject.supportedInterfaceOrientations]; - [QMUIHelper rotateToDeviceOrientation:deviceOrientationToRotate]; - }); - }); -} - -- (BOOL)qmui_shouldForceRotateDeviceOrientation { - return NO; -} - -@end - @implementation UIViewController (QMUINavigationController) QMUISynthesizeBOOLProperty(qmui_navigationControllerPopGestureRecognizerChanging, setQmui_navigationControllerPopGestureRecognizerChanging) @@ -484,124 +565,31 @@ - (BOOL)qmui_navigationControllerPoppingInteracted { return self.qmui_poppingByInteractivePopGestureRecognizer || self.qmui_willAppearByInteractivePopGestureRecognizer; } +- (void)qmui_animateAlongsideTransition:(void (^ __nullable)(id context))animation + completion:(void (^ __nullable)(id context))completion { + if (self.transitionCoordinator) { + BOOL animationQueuedToRun = [self.transitionCoordinator animateAlongsideTransition:animation completion:completion]; + // 某些情况下传给 animateAlongsideTransition 的 animation 不会被执行,这时候要自己手动调用一下 + // 但即便如此,completion 也会在动画结束后才被调用,因此这样写不会导致 completion 比 animation block 先调用 + // 某些情况包含:从 B 手势返回 A 的过程中,取消手势,animation 不会被调用 + // https://github.com/Tencent/QMUI_iOS/issues/692 + if (!animationQueuedToRun && animation) { + animation(nil); + } + } else { + if (animation) animation(nil); + if (completion) completion(nil); + } +} + @end @implementation QMUIHelper (ViewController) + (nullable UIViewController *)visibleViewController { - UIViewController *rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController; + UIViewController *rootViewController = UIApplication.sharedApplication.delegate.window.rootViewController; UIViewController *visibleViewController = [rootViewController qmui_visibleViewControllerIfExist]; return visibleViewController; } @end - -// 为了 UIViewController 适配 iOS 11 下出现不透明的 tabBar 时底部 inset 错误的问题而创建的 Category -// https://github.com/Tencent/QMUI_iOS/issues/218 -@interface UITabBar (NavigationController) - -@end - -@implementation UITabBar (NavigationController) - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - if (@available(iOS 11, *)) { - - OverrideImplementation([UITabBar class], @selector(setHidden:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UITabBar *selfObject, BOOL hidden) { - - // call super - void (^callSuperBlock)(BOOL) = ^void(BOOL aHidden) { - void (*originSelectorIMP)(id, SEL, BOOL); - originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, aHidden); - }; - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - BOOL shouldNotify = selfObject.hidden != hidden; - callSuperBlock(hidden); - if (shouldNotify) { - [[NSNotificationCenter defaultCenter] postNotificationName:QMUITabBarStyleChangedNotification object:selfObject]; - } - } else { - callSuperBlock(hidden); - } - }; - }); - - OverrideImplementation([UITabBar class], @selector(setBackgroundImage:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UITabBar *selfObject, UIImage *backgroundImage) { - - // call super - void (^callSuperBlock)(UIImage *) = ^void(UIImage *aBackgroundImage) { - void (*originSelectorIMP)(id, SEL, UIImage *); - originSelectorIMP = (void (*)(id, SEL, UIImage *))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, aBackgroundImage); - }; - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - BOOL shouldNotify = ![selfObject.backgroundImage isEqual:backgroundImage]; - callSuperBlock(backgroundImage); - if (shouldNotify) { - [[NSNotificationCenter defaultCenter] postNotificationName:QMUITabBarStyleChangedNotification object:selfObject]; - } - } else { - callSuperBlock(backgroundImage); - } - }; - }); - - OverrideImplementation([UITabBar class], @selector(setTranslucent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UITabBar *selfObject, BOOL translucent) { - - // call super - void (^callSuperBlock)(BOOL) = ^void(BOOL aTranslucent) { - void (*originSelectorIMP)(id, SEL, BOOL); - originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, aTranslucent); - }; - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - BOOL shouldNotify = selfObject.translucent != translucent; - callSuperBlock(translucent); - if (shouldNotify) { - [[NSNotificationCenter defaultCenter] postNotificationName:QMUITabBarStyleChangedNotification object:selfObject]; - } - } else { - callSuperBlock(translucent); - } - }; - }); - - OverrideImplementation([UITabBar class], @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { - return ^(UITabBar *selfObject, CGRect frame) { - - // call super - void (^callSuperBlock)(CGRect) = ^void(CGRect aFrame) { - void (*originSelectorIMP)(id, SEL, CGRect); - originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); - originSelectorIMP(selfObject, originCMD, aFrame); - }; - - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - BOOL shouldNotify = CGRectGetMinY(selfObject.frame) != CGRectGetMinY(frame); - callSuperBlock(frame); - if (shouldNotify) { - [[NSNotificationCenter defaultCenter] postNotificationName:QMUITabBarStyleChangedNotification object:selfObject]; - } - } else { - callSuperBlock(frame); - } - }; - }); - } - }); -} - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIVisualEffectView+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIVisualEffectView+QMUI.h new file mode 100644 index 00000000..dc8a994f --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIVisualEffectView+QMUI.h @@ -0,0 +1,33 @@ +/** + * 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. + */ + +// +// UIVisualEffectView+QMUI.h +// QMUIKit +// +// Created by MoLice on 2020/7/15. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIVisualEffectView (QMUI) + +/** + 系统的 UIVisualEffectView 会为不同的 effect 生成不同的 subview 并为其设置对应的 backgroundColor、alpha,这些 subview 的样式我们是修改不了的,如果有设计需求希望在磨砂上方盖一层前景色来调整磨砂效果,总是会受自带的 subview 的影响(例如无法有特别明显的磨砂效果,因为自带的 subview alpha 可能很高,透不过去),因此增加这个属性,当设置一个非 nil 的颜色后,会强制把系统自带的 subview 隐藏掉,只显示你自己的 foregroundColor,从而实现精准的调整。 + + 以 UINavigationBar 为例,当我们通过 UINavigationBar.barTintColor 或者 UINavigationBarAppearance.backgroundEffect/backgroundColor 实现磨砂效果时,我们设置上去的 barTintColor 最终会被系统进行一些运算后产生另一个色值,最终显示出来的色值和我们设置的 barTintColor 是相似但不相等的,如果希望有精准的色值调整,就可以自己获取 UINavigationBar 内部的 UIVisualEffectView,再修改它的 qmui_foregroundColor。 + + @note 注意这个颜色需要是半透明的,才能透出背后的磨砂,如果设置不透明的色值,就失去了磨砂效果了。 + @note 注意如果开启了系统的“降低透明度”辅助功能开关,此时 qmui_foregroundColor 的效果会变得比较怪异,因此默认会监听 UIAccessibilityIsReduceTransparencyEnabled 的变化,当开启时会强制把 qmui_foregroundColor 改为不透明的,从而屏蔽磨砂的效果。 + */ +@property(nonatomic, strong, nullable) UIColor *qmui_foregroundColor; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIVisualEffectView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIVisualEffectView+QMUI.m new file mode 100644 index 00000000..cc7e7358 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIVisualEffectView+QMUI.m @@ -0,0 +1,151 @@ +/** + * 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. + */ + +// +// UIVisualEffectView+QMUI.m +// QMUIKit +// +// Created by MoLice on 2020/7/15. +// + +#import "UIVisualEffectView+QMUI.h" +#import "QMUICore.h" +#import "CALayer+QMUI.h" + +@interface UIView (QMUI_VisualEffectView) + +// 为了方便,这个属性声明在 UIView 里,但实际上只有两个私有的 Visual View 会用到 +@property(nonatomic, assign) BOOL qmuive_keepHidden; +@end + +@interface UIVisualEffectView () + +@property(nonatomic, strong) CALayer *qmuive_foregroundLayer; +@property(nonatomic, assign, readonly) BOOL qmuive_showsForegroundLayer; +@end + +@implementation UIVisualEffectView (QMUI) + +QMUISynthesizeIdStrongProperty(qmuive_foregroundLayer, setQmuive_foregroundLayer) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation([UIVisualEffectView class], @selector(didAddSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIVisualEffectView *selfObject, UIView *firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, UIView *); + originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + [selfObject qmuive_updateSubviews]; + }; + }); + + ExtendImplementationOfVoidMethodWithoutArguments([UIVisualEffectView class], @selector(layoutSubviews), ^(UIVisualEffectView *selfObject) { + if (selfObject.qmuive_showsForegroundLayer) { + selfObject.qmuive_foregroundLayer.frame = selfObject.bounds; + } + }); + }); +} + +static char kAssociatedObjectKey_foregroundColor; +- (void)setQmui_foregroundColor:(UIColor *)qmui_foregroundColor { + objc_setAssociatedObject(self, &kAssociatedObjectKey_foregroundColor, qmui_foregroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_foregroundColor && !self.qmuive_foregroundLayer) { + self.qmuive_foregroundLayer = [CALayer layer]; + [self.qmuive_foregroundLayer qmui_removeDefaultAnimations]; + [self.layer addSublayer:self.qmuive_foregroundLayer]; + + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityReduceTransparencyStatusDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleReduceTransparencyStatusDidChangeNotification:) name:UIAccessibilityReduceTransparencyStatusDidChangeNotification object:nil]; + } + if (self.qmuive_foregroundLayer) { + if (UIAccessibilityIsReduceTransparencyEnabled()) { + qmui_foregroundColor = [qmui_foregroundColor colorWithAlphaComponent:1]; + } + self.qmuive_foregroundLayer.backgroundColor = qmui_foregroundColor.CGColor; + self.qmuive_foregroundLayer.hidden = !qmui_foregroundColor; + [self qmuive_updateSubviews]; + [self setNeedsLayout]; + } +} + +- (UIColor *)qmui_foregroundColor { + return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_foregroundColor); +} + +- (BOOL)qmuive_showsForegroundLayer { + return self.qmuive_foregroundLayer && !self.qmuive_foregroundLayer.hidden; +} + +- (void)qmuive_updateSubviews { + if (self.qmuive_foregroundLayer) { + + // 先放在最背后,然后在遇到磨砂的 backdropLayer 时再放到它前面,因为有些情况下可能不存在 backdropLayer(例如 effect = nil 或者 effect 为 UIVibrancyEffect) + [self.layer qmui_sendSublayerToBack:self.qmuive_foregroundLayer]; + for (NSInteger i = 0; i < self.layer.sublayers.count; i++) { + CALayer *sublayer = self.layer.sublayers[i]; + if ([NSStringFromClass(sublayer.class) isEqualToString:@"UICABackdropLayer"]) { + [self.layer insertSublayer:self.qmuive_foregroundLayer above:sublayer]; + break; + } + } + + [self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) { + NSString *className = NSStringFromClass(subview.class); + if ([className isEqualToString:@"_UIVisualEffectSubview"] || [className isEqualToString:@"_UIVisualEffectFilterView"]) { + subview.qmuive_keepHidden = !self.qmuive_foregroundLayer.hidden; + } + }]; + } +} + +- (void)handleReduceTransparencyStatusDidChangeNotification:(NSNotification *)notification { + if (self.qmui_foregroundColor) { + self.qmui_foregroundColor = self.qmui_foregroundColor; + } +} + +@end + +@implementation UIView (QMUI_VisualEffectView) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation(NSClassFromString(@"_UIVisualEffectSubview"), @selector(setHidden:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, BOOL firstArgv) { + + if (selfObject.qmuive_keepHidden) { + firstArgv = YES; + } + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + }; + }); + }); +} + +static char kAssociatedObjectKey_keepHidden; +- (void)setQmuive_keepHidden:(BOOL)qmuive_keepHidden { + objc_setAssociatedObject(self, &kAssociatedObjectKey_keepHidden, @(qmuive_keepHidden), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + // 从语义来看,当 keepHidden = NO 时,并不意味着 hidden 就一定要为 NO,但为了方便添加了 foregroundColor 后再去除 foregroundColor 时做一些恢复性质的操作,这里就实现成 keepHidden = NO 时 hidden = NO + self.hidden = qmuive_keepHidden; +} + +- (BOOL)qmuive_keepHidden { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_keepHidden)) boolValue]; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIWindow+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIWindow+QMUI.h index af7a6af8..41745ca0 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIWindow+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIWindow+QMUI.h @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIWindow+QMUI.h @@ -28,4 +28,14 @@ @warning 如果你自己创建的 window 不满足以上2点,那么就算 qmui_capturesStatusBarAppearance 为 YES,也无法得到 statusBar 的控制权。 */ @property(nonatomic, assign) BOOL qmui_capturesStatusBarAppearance; + +/** + 1. 支持以 property 形式修改值,但不支持重写 getter 来修改。 + 2. 对低于 iOS 15 的系统也支持。 + */ +@property(nonatomic, assign) BOOL qmui_canBecomeKeyWindow; + +/// 当前 window 因各种原因(例如其他 window 显式调用 makeKey、当前 keyWindow 被隐藏导致系统自动流转 keyWindow、主动向自身调用 resignKeyWindow 等)导致从 keyWindow 转变为非 keyWindow 时会询问这个 block,业务可在这个 block 里干预当前的流转。 +/// 实际场景例如,背后 window 正在显示一个带输入框的 webView 网页,输入框聚焦以升起键盘,此时你再新开一个更高 windowLevel 的 window,盖在 webView 上并且 makeKey,就会发现你的 window 依然被键盘挡住,因为 webView 有个特性是如果有输入框聚焦,则 webView 内部会不断地尝试将输入框 becomeFirstResponder 并且让输入框所在的 window makeKey,这就会抢占了我们刚刚手动盖上来的 window 的 key,所以此时就可以给新开的 window 使用本 block,返回 NO,使 webView 无法抢占 keyWindow,从而避免键盘遮挡。 +@property(nonatomic, copy) BOOL (^qmui_canResignKeyWindowBlock)(UIWindow *selfObject, UIWindow *windowWillBecomeKey); @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIWindow+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIWindow+QMUI.m index 992fe1ba..57e03b66 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIWindow+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIWindow+QMUI.m @@ -1,10 +1,10 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ // // UIWindow+QMUI.m @@ -24,33 +24,108 @@ + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ + // -[UIWindow initWithFrame:] ExtendImplementationOfNonVoidMethodWithSingleArgument([UIWindow class], @selector(initWithFrame:), CGRect, UIWindow *, ^UIWindow *(UIWindow *selfObject, CGRect frame, UIWindow *originReturnValue) { selfObject.qmui_capturesStatusBarAppearance = YES; return originReturnValue; }); + + // -[UIWindow initWithWindowScene:] + ExtendImplementationOfNonVoidMethodWithSingleArgument([UIWindow class], @selector(initWithWindowScene:), UIWindowScene *, UIWindow *, ^UIWindow *(UIWindow *selfObject, UIWindowScene *windowScene, UIWindow *originReturnValue) { + selfObject.qmui_capturesStatusBarAppearance = YES; + return originReturnValue; + }); + // -[UIWindow _canAffectStatusBarAppearance] OverrideImplementation([UIWindow class], NSSelectorFromString([NSString stringWithFormat:@"_%@%@%@", @"canAffect", @"StatusBar", @"Appearance"]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^BOOL(UIWindow *selfObject) { - // call super - BOOL (^callSuperBlock)(void) = ^BOOL{ + + if (selfObject.qmui_capturesStatusBarAppearance) { + // call super BOOL (*originSelectorIMP)(id, SEL); originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider(); BOOL result = originSelectorIMP(selfObject, originCMD); return result; - }; + } - // avoid superclass - if ([selfObject isKindOfClass:originClass]) { - if (selfObject.qmui_capturesStatusBarAppearance) { - return callSuperBlock(); + return NO; + }; + }); + }); +} + +static char kAssociatedObjectKey_canBecomeKeyWindow; +- (void)setQmui_canBecomeKeyWindow:(BOOL)qmui_canBecomeKeyWindow { + [self qmuiw_hookIfNeeded]; + objc_setAssociatedObject(self, &kAssociatedObjectKey_canBecomeKeyWindow, @(qmui_canBecomeKeyWindow), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (BOOL)qmui_canBecomeKeyWindow { + NSNumber *value = objc_getAssociatedObject(self, &kAssociatedObjectKey_canBecomeKeyWindow); + if (!value) { + return YES; + } + return value.boolValue; +} + +static char kAssociatedObjectKey_canResignKeyWindowBlock; +- (void)setQmui_canResignKeyWindowBlock:(BOOL (^)(UIWindow *, UIWindow *))qmui_canResignKeyWindowBlock { + [self qmuiw_hookIfNeeded]; + objc_setAssociatedObject(self, &kAssociatedObjectKey_canResignKeyWindowBlock, qmui_canResignKeyWindowBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); +} + +- (BOOL (^)(UIWindow *, UIWindow *))qmui_canResignKeyWindowBlock { + return (BOOL (^)(UIWindow *, UIWindow *))objc_getAssociatedObject(self, &kAssociatedObjectKey_canResignKeyWindowBlock); +} + +- (void)qmuiw_hookIfNeeded { + [QMUIHelper executeBlock:^{ + // - [UIWindow canBecomeKeyWindow] + SEL sel1 = @selector(canBecomeKeyWindow); + // - [UIWindow _canBecomeKeyWindow] + SEL sel2 = NSSelectorFromString([NSString stringWithFormat:@"_%@", NSStringFromSelector(sel1)]); + SEL sel = [self respondsToSelector:sel1] ? sel1 : ([self respondsToSelector:sel2] ? sel2 : nil); + if (sel) { + OverrideImplementation([UIWindow class], sel, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^BOOL(UIWindow *selfObject) { + // call super + BOOL (*originSelectorIMP)(id, SEL); + originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider(); + BOOL result = originSelectorIMP(selfObject, originCMD); + + BOOL hasSet = !!objc_getAssociatedObject(selfObject, &kAssociatedObjectKey_canBecomeKeyWindow); + if (hasSet) { + result = selfObject.qmui_canBecomeKeyWindow; } - return NO; + + BeginIgnoreDeprecatedWarning + UIWindow *keyWindow = UIApplication.sharedApplication.keyWindow; + if (result && keyWindow && keyWindow != selfObject && keyWindow.qmui_canResignKeyWindowBlock) { + result = keyWindow.qmui_canResignKeyWindowBlock(keyWindow, selfObject); + } + EndIgnoreDeprecatedWarning + + return result; + }; + }); + } else { + QMUIAssert(NO, @"UIWindow (QMUI)", @"%f 不存在方法 -[UIWindow _canBecomeKeyWindow]", IOS_VERSION); + } + + OverrideImplementation([UIWindow class], @selector(resignKeyWindow), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIWindow *selfObject) { + + if (selfObject.isKeyWindow && selfObject.qmui_canResignKeyWindowBlock && !selfObject.qmui_canResignKeyWindowBlock(selfObject, selfObject)) { + return; } - return callSuperBlock(); + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); }; }); - }); + } oncePerIdentifier:@"UIWindow (QMUI) keyWindow"]; } @end diff --git a/QMUI/QMUIKitTests/Components/QMUIThemeTests.m b/QMUI/QMUIKitTests/Components/QMUIThemeTests.m new file mode 100644 index 00000000..928c635d --- /dev/null +++ b/QMUI/QMUIKitTests/Components/QMUIThemeTests.m @@ -0,0 +1,82 @@ +/** + * 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. + */ +// +// QMUIThemeTests.m +// QMUIKitTests +// +// Created by MoLice on 2019/J/27. +// + +#import +#import + +@interface QMUIThemeTests : XCTestCase + +@end + +@implementation QMUIThemeTests + +- (void)testUIColorMethods { + UIColor *color = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return UIColorWhite; + }]; + XCTAssertNoThrow([color set]); + XCTAssertNoThrow([color setFill]); + XCTAssertNoThrow([color setStroke]); + + CGFloat white; + CGFloat alpha; + XCTAssertNoThrow([color getWhite:&white alpha:&alpha]); + XCTAssertTrue(betweenOrEqual(.9, white, 1));// 由于精度问题...先这么写吧 + XCTAssertEqual(alpha, 1); + + CGFloat hue; + CGFloat saturation; + CGFloat brightness; + CGFloat alpha2; + XCTAssertNoThrow([color getHue:&hue saturation:&saturation brightness:&brightness alpha:&alpha2]); + XCTAssertTrue(hue == 0 || hue == 1); + XCTAssertTrue(saturation = 1); + XCTAssertTrue(brightness = 1); + XCTAssertTrue(alpha2 = 1); + + CGFloat red; + CGFloat green; + CGFloat blue; + CGFloat alpha3; + XCTAssertNoThrow([color getRed:&red green:&green blue:&blue alpha:&alpha3]); + XCTAssertEqual(red, 1); + XCTAssertEqual(green, 1); + XCTAssertEqual(blue, 1); + XCTAssertEqual(alpha3, 1); + + XCTAssertNoThrow([color colorWithAlphaComponent:.5]); + CGFloat alpha4; + UIColor *colorWithAlpha = [color colorWithAlphaComponent:.5]; + [colorWithAlpha getRed:nil green:nil blue:nil alpha:&alpha4]; + XCTAssertEqual(alpha4, .5); + + XCTAssertNoThrow(color.CGColor); + XCTAssertFalse(color.CGColor == nil); + + XCTAssertNoThrow([color copy]); + XCTAssertTrue(((UIColor *)[color copy]).qmui_isQMUIDynamicColor); + + XCTAssertNoThrow([color isEqual:nil]); + XCTAssertTrue([color isEqual:color]); + XCTAssertFalse([color isEqual:[UIColor whiteColor]]); + + XCTAssertEqual(([NSSet setWithObjects:color, color, nil]).count, 1); + XCTAssertEqual(([NSSet setWithObjects:color, color.copy, nil]).count, 2); +} + +- (void)testQMUIMethods { + +} + +@end diff --git a/QMUI/QMUIKitTests/Core/QMUICommonDefinesTests.m b/QMUI/QMUIKitTests/Core/QMUICommonDefinesTests.m new file mode 100644 index 00000000..5cf3eb09 --- /dev/null +++ b/QMUI/QMUIKitTests/Core/QMUICommonDefinesTests.m @@ -0,0 +1,44 @@ +/** + * 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. + */ + +// +// QMUICommonDefinesTests.m +// QMUIKitTests +// +// Created by MoLice on 2020/5/12. +// + +#import +#import + +@interface QMUICommonDefinesTests : XCTestCase + +@end + +@implementation QMUICommonDefinesTests + +- (void)testCGFloatCalcOperator { + CGFloat a = 0.999; + CGFloat b = 1.011; + CGFloat c = 1.033; + CGFloat d = 1.099; + + XCTAssertTrue(CGFloatEqualToFloat(a, b)); + XCTAssertTrue(CGFloatEqualToFloat(b, c)); + XCTAssertTrue(CGFloatEqualToFloat(c, d)); + + XCTAssertTrue(CGFloatEqualToFloatWithPrecision(a, b, 1)); + XCTAssertTrue(CGFloatEqualToFloatWithPrecision(b, c, 1)); + XCTAssertFalse(CGFloatEqualToFloatWithPrecision(c, d, 1)); + + XCTAssertFalse(CGFloatEqualToFloatWithPrecision(a, b, 2)); + XCTAssertFalse(CGFloatEqualToFloatWithPrecision(b, c, 2)); + XCTAssertFalse(CGFloatEqualToFloatWithPrecision(c, d, 2)); +} + +@end diff --git a/QMUI/QMUIKitTests/Info.plist b/QMUI/QMUIKitTests/Info.plist new file mode 100644 index 00000000..6c40a6cd --- /dev/null +++ b/QMUI/QMUIKitTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/QMUI/QMUIKitTests/UIKitExtensions/NSObjectTests.m b/QMUI/QMUIKitTests/UIKitExtensions/NSObjectTests.m new file mode 100644 index 00000000..0451c9eb --- /dev/null +++ b/QMUI/QMUIKitTests/UIKitExtensions/NSObjectTests.m @@ -0,0 +1,50 @@ +/** + * 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. + */ +// +// NSObject.m +// QMUIKitTests +// +// Created by MoLice on 2019/J/5. +// + +#import +#import + +@interface NSObjectTests : XCTestCase + +@end + +@implementation NSObjectTests + +- (void)testValueForKey { + UINavigationBar *navigationBar = [UINavigationBar new]; + [navigationBar sizeToFit]; + XCTAssertTrue(navigationBar.qmui_backgroundView); + XCTAssertFalse(navigationBar.qmui_shadowImageView); + + UITabBar *tabBar = [UITabBar new]; + [tabBar sizeToFit]; + XCTAssertTrue(tabBar.qmui_backgroundView); + XCTAssertFalse(tabBar.qmui_shadowImageView); + + UISearchBar *searchBar = [UISearchBar new]; + searchBar.scopeButtonTitles = @[@"A", @"B"]; + searchBar.showsCancelButton = YES; + [searchBar sizeToFit]; + [searchBar qmui_setValue:@"Test" forKey:@"_cancelButtonText"]; + // iOS13 crash : [searchBar setValue:@"Test" forKey:@"_cancelButtonText"]; + UIView *searchField = [searchBar qmui_valueForKey:@"_searchField"]; + // iOS13 crash : [searchBar valueForKey:@"_searchField"]; + + XCTAssertTrue(searchBar.qmui_backgroundView); + XCTAssertTrue(searchBar.qmui_cancelButton); + XCTAssertTrue(searchBar.qmui_segmentedControl); + XCTAssertFalse([searchBar qmui_valueForKey:@"_searchController"]); +} + +@end diff --git a/QMUI/QMUIKitTests/UIKitExtensions/NSStringTests.m b/QMUI/QMUIKitTests/UIKitExtensions/NSStringTests.m new file mode 100644 index 00000000..8a0bb5bf --- /dev/null +++ b/QMUI/QMUIKitTests/UIKitExtensions/NSStringTests.m @@ -0,0 +1,311 @@ +/** + * 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. + */ +// +// NSStringTests.m +// QMUIKitTests +// +// Created by MoLice on 2021/4/1. +// + +#import +#import + +@interface NSStringTests : XCTestCase + +@end + +@implementation NSStringTests + +- (void)testStringSafety { + // 系统标注了 string 参数 nonnull,如果传了 nil 会 crash,QMUIStringPrivate 里对 nil 做了保护 + BeginIgnoreClangWarning(-Wnonnull) + XCTAssertNoThrow([[NSAttributedString alloc] initWithString:nil]); + XCTAssertNoThrow([[NSAttributedString alloc] initWithString:nil attributes:nil]); + XCTAssertNoThrow([[NSMutableAttributedString alloc] initWithString:nil]); + XCTAssertNoThrow([[NSMutableAttributedString alloc] initWithString:nil attributes:nil]); + EndIgnoreClangWarning + + NSString *string = @"A😊B"; + + XCTAssertNoThrow([string substringFromIndex:0]); + XCTAssertNoThrow([string substringFromIndex:string.length]); // 系统自身对 length 的参数做了保护,返回空字符串 + XCTAssertThrows([string substringFromIndex:5]); // 越界的识别 + XCTAssertNoThrow([string substringFromIndex:1]); + XCTAssertThrows([string substringFromIndex:2]); // emoji 中间裁剪的识别 + XCTAssertNoThrow([string substringFromIndex:3]); + + XCTAssertNoThrow([string substringToIndex:0]); + XCTAssertNoThrow([string substringToIndex:string.length]); // toIndex 所在的字符不包含在返回结果里,所以允许传入 string.length 的位置 + XCTAssertThrows([string substringToIndex:string.length + 1]); // 越界的识别 + XCTAssertNoThrow([string substringToIndex:1]); + XCTAssertThrows([string substringToIndex:2]);// emoji 中间裁剪的识别 + XCTAssertNoThrow([string substringToIndex:3]); + + XCTAssertNoThrow([string substringWithRange:NSMakeRange(0, 0)]); + XCTAssertNoThrow([string substringWithRange:NSMakeRange(string.length, 0)]); + XCTAssertThrows([string substringWithRange:NSMakeRange(string.length, 1)]); // 越界的识别 + XCTAssertNoThrow([string substringWithRange:NSMakeRange(1, 2)]); + XCTAssertThrows([string substringWithRange:NSMakeRange(1, 1)]); // emoji 中间裁剪的识别 +} + +- (void)testStringMatching { + NSString *string = @"string0.05"; + XCTAssertNil([string qmui_stringMatchedByPattern:@""]); + XCTAssertNotNil([string qmui_stringMatchedByPattern:@"str"]); + XCTAssertEqualObjects([string qmui_stringMatchedByPattern:@"[\\d\\.]+"], @"0.05"); + + XCTAssertNil([string qmui_stringMatchedByPattern:@"str" groupIndex:1]); + XCTAssertEqualObjects([string qmui_stringMatchedByPattern:@"ing([\\d\\.]+)" groupIndex:1], @"0.05"); + + XCTAssertNil([string qmui_stringMatchedByPattern:@"str" groupName:@"number"]); + XCTAssertEqualObjects([string qmui_stringMatchedByPattern:@"ing(?[\\d\\.]+)" groupName:@"number"], @"0.05"); + XCTAssertThrows([string qmui_stringMatchedByPattern:@"ing(?[\\d\\.]+)" groupName:@"num"]); +} + +- (void)testSubstring1 { + NSString *text = @"01234567890123456789"; // length = 20, 20 + NSString *zh = @"零一二三四五六七八九"; // length = 10, 20; + NSString *emoji = @"😊😊😊😊😊😊😊😊😊😊";// length = 20, 20 + + NSInteger toIndex = 7; + BOOL lessValue = YES;// 系统的 substring 默认就是 lessValue = YES,也即 toIndex 所在位置的字符是不包含在返回结果里的 + BOOL countingNonASCIICharacterAsTwo = NO; + + NSString *text2 = [text qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(text2.length, toIndex); + + NSString *zh2 = [zh qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(zh2.length, toIndex); + NSString *zh3 = [zh substringToIndex:toIndex]; + XCTAssertTrue((lessValue && zh2.length == zh3.length) || (!lessValue && zh2.length > zh3.length)); + + NSString *emoji2 = [emoji qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + NSString *emoji3 = [emoji substringToIndex:[emoji rangeOfComposedCharacterSequenceAtIndex:toIndex].location]; + XCTAssertTrue((lessValue && emoji2.length == emoji3.length) || (!lessValue && emoji2.length > emoji3.length)); +} + +- (void)testSubstring2 { + NSString *text = @"01234567890123456789"; // length = 20, 20 + NSString *zh = @"零一二三四五六七八九"; // length = 10, 20; + NSString *emoji = @"😊😊😊😊😊😊😊😊😊😊";// length = 20, 20 + + NSInteger toIndex = 14; + BOOL lessValue = YES; + BOOL countingNonASCIICharacterAsTwo = YES; + + NSString *text2 = [text qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(text2.length, toIndex); + + NSString *zh2 = [zh qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(zh2.qmui_lengthWhenCountingNonASCIICharacterAsTwo, (toIndex / 2) * 2); + NSString *zh3 = [zh substringToIndex:toIndex / 2]; + XCTAssertTrue(zh2.length == zh3.length && zh2.qmui_lengthWhenCountingNonASCIICharacterAsTwo == zh3.length * 2); + + NSString *emoji2 = [emoji qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + NSString *emoji3 = [emoji substringToIndex:[emoji rangeOfComposedCharacterSequenceAtIndex:toIndex / 2].location]; + XCTAssertTrue((lessValue && emoji2.length == emoji3.length) || (!lessValue && emoji2.length > emoji3.length)); +} + +- (void)testSubstring3 { + NSString *text = @"01234567890123456789"; // length = 20, 20 + NSString *zh = @"零一二三四五六七八九"; // length = 10, 20; + NSString *emoji = @"😊😊😊😊😊😊😊😊😊😊";// length = 20, 20 + + NSInteger toIndex = 15; + BOOL lessValue = YES; + BOOL countingNonASCIICharacterAsTwo = YES; + + NSString *text2 = [text qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(text2.length, toIndex); + + NSString *zh2 = [zh qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(zh2.qmui_lengthWhenCountingNonASCIICharacterAsTwo, (toIndex / 2) * 2); + NSString *zh3 = [zh substringToIndex:toIndex / 2]; + XCTAssertTrue(zh2.length == zh3.length && zh2.qmui_lengthWhenCountingNonASCIICharacterAsTwo == zh3.length * 2); + + NSString *emoji2 = [emoji qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + NSString *emoji3 = [emoji substringToIndex:[emoji rangeOfComposedCharacterSequenceAtIndex:toIndex / 2].location]; + XCTAssertTrue((lessValue && emoji2.length == emoji3.length) || (!lessValue && emoji2.length > emoji3.length)); +} + +- (void)testSubstring4 { + NSString *text = @"01234567890123456789"; // length = 20, 20 + NSString *zh = @"零一二三四五六七八九"; // length = 10, 20; + NSString *emoji = @"😊😊😊😊😊😊😊😊😊😊";// length = 20, 20 + + NSInteger toIndex = 7; + BOOL lessValue = NO; + BOOL countingNonASCIICharacterAsTwo = NO; + + NSString *text2 = [text qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(text2.length, toIndex); + + NSString *zh2 = [zh qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(zh2.length, toIndex); + + NSString *zh3 = [zh substringToIndex:toIndex]; + XCTAssertTrue((!countingNonASCIICharacterAsTwo && zh2.length == zh3.length) || (countingNonASCIICharacterAsTwo && zh2.length > zh3.length)); + + NSString *emoji2 = [emoji qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + NSString *emoji3 = [emoji substringToIndex:[emoji rangeOfComposedCharacterSequenceAtIndex:toIndex].location]; + XCTAssertTrue((lessValue && emoji2.length == emoji3.length) || (!lessValue && emoji2.length > emoji3.length)); +} + +- (void)testSubstring5 { + NSString *text = @"01234567890123456789"; // length = 20, 20 + NSString *zh = @"零一二三四五六七八九"; // length = 10, 20; + NSString *emoji = @"😊😊😊😊😊😊😊😊😊😊";// length = 20, 20 + + NSInteger toIndex = 14; + BOOL lessValue = NO; + BOOL countingNonASCIICharacterAsTwo = YES; + + NSString *text2 = [text qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(text2.length, toIndex + 1); + + NSString *zh2 = [zh qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(zh2.qmui_lengthWhenCountingNonASCIICharacterAsTwo, (toIndex / 2 + 1) * 2); + NSString *zh3 = [zh substringToIndex:toIndex / 2]; + XCTAssertTrue(zh2.length == zh3.length + 1); + XCTAssertEqual(zh2.qmui_lengthWhenCountingNonASCIICharacterAsTwo, (zh3.length + 1) * 2); + + NSString *emoji2 = [emoji qmui_substringAvoidBreakingUpCharacterSequencesToIndex:toIndex lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + NSString *emoji3 = [emoji substringToIndex:[emoji rangeOfComposedCharacterSequenceAtIndex:toIndex].location]; + XCTAssertEqual(emoji2.length, emoji3.length / 2 + 1); +} + +- (void)testSubstring6 { + NSString *emoji = @"😡😊😞😊😊😊😊😊😊😊";// length = 20, 20 + NSRange range = NSMakeRange(1, 6); + BOOL lessValue = YES; + BOOL countingNonASCIICharacterAsTwo = NO; + NSString *emoji2 = [emoji qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(emoji2.length, 4); + + lessValue = NO; + emoji2 = [emoji qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(emoji2.length, 8); + + range = NSMakeRange(0, 6); + lessValue = YES; + emoji2 = [emoji qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(emoji2.length, 6); + + lessValue = NO; + emoji2 = [emoji qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(emoji2.length, 6); + + range = NSMakeRange(0, 1); + lessValue = YES; + emoji2 = [emoji qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(emoji2.length, 0); + + lessValue = NO; + emoji2 = [emoji qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(emoji2.length, 2); + + NSString *text = @"01234567890123456789"; // length = 20, 20 + NSString *zh = @"零一二三四五六七八九"; // length = 10, 20; + range = NSMakeRange(3, 5); + lessValue = YES; + NSString *text2 = [text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(text2.length, range.length); + + NSString *zh2 = [zh qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(zh2.length, range.length); + NSString *zh3 = [zh substringWithRange:range]; + XCTAssertTrue(zh2.length == zh3.length); + + countingNonASCIICharacterAsTwo = YES; + + text2 = [text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(text2.length, range.length); + + zh2 = [zh qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(zh2.length, 2); + + range = NSMakeRange(3, 6); + + text2 = [text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(text2.length, range.length); + + zh2 = [zh qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(zh2.length, 2); + + lessValue = NO; + + text2 = [text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(text2.length, range.length); + + zh2 = [zh qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(zh2.length, 4); + + zh = @"零一二三4五六七八九"; // length = 10, 19; + lessValue = YES; + + zh2 = [zh qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; + XCTAssertEqual(zh2.length, 3); +} + +// NSAttributedString 的简单处理,只要和 NSString 一致就行了 +- (void)testAttributedString { + NSArray *strs = @[ + [[NSAttributedString alloc] initWithString:@"01234567890123456789"],// length = 20, 20 + [[NSAttributedString alloc] initWithString:@"零一二三四五六七八九"],// length = 10, 20; + [[NSAttributedString alloc] initWithString:@"😡😊😞😊😊😊😊😊😊😊"],// length = 20, 20 + ]; + + void (^testingBlock)(NSAttributedString *, BOOL, BOOL) = ^void(NSAttributedString *str, BOOL lessValue, BOOL asTwo) { + XCTAssertEqualObjects( + [str qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:7 lessValue:lessValue countingNonASCIICharacterAsTwo:asTwo].string, + [str.string qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:7 lessValue:lessValue countingNonASCIICharacterAsTwo:asTwo]); + + XCTAssertEqualObjects( + [str qmui_substringAvoidBreakingUpCharacterSequencesToIndex:7 lessValue:lessValue countingNonASCIICharacterAsTwo:asTwo].string, + [str.string qmui_substringAvoidBreakingUpCharacterSequencesToIndex:7 lessValue:lessValue countingNonASCIICharacterAsTwo:asTwo]); + + XCTAssertEqualObjects( + [str qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(3, 6) lessValue:lessValue countingNonASCIICharacterAsTwo:asTwo].string, + [str.string qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(3, 6) lessValue:lessValue countingNonASCIICharacterAsTwo:asTwo]); + }; + + [strs enumerateObjectsUsingBlock:^(NSAttributedString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + testingBlock(obj, YES, NO); + testingBlock(obj, YES, YES); + testingBlock(obj, NO, NO); + testingBlock(obj, NO, YES); + }]; +} + +- (void)testAttributedString2 { + NSAttributedString *nilString = nil; + NSAttributedString *emptyString = NSAttributedString.new; + NSAttributedString *emptyString2 = [[NSAttributedString alloc] initWithString:@"" attributes:@{NSFontAttributeName: UIFontMake(16), NSParagraphStyleAttributeName: [NSParagraphStyle qmui_paragraphStyleWithLineHeight:20 lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentCenter]}]; + NSAttributedString *paraString = [[NSAttributedString alloc] initWithString:@"你好啊" attributes:@{NSParagraphStyleAttributeName: [NSParagraphStyle qmui_paragraphStyleWithLineHeight:20 lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentCenter]}]; + NSAttributedString *nonParaString = [[NSAttributedString alloc] initWithString:@"你好啊" attributes:@{NSFontAttributeName: UIFontMake(16)}]; + NSMutableAttributedString *multiParaString = [[NSMutableAttributedString alloc] initWithString:@"片段1片段2" attributes:@{NSFontAttributeName: UIFontMake(16)}]; + [multiParaString addAttribute:NSParagraphStyleAttributeName value:[NSParagraphStyle qmui_paragraphStyleWithLineHeight:20 lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentCenter] range:NSMakeRange(0, 3)]; + [multiParaString addAttribute:NSParagraphStyleAttributeName value:[NSParagraphStyle qmui_paragraphStyleWithLineHeight:40 lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentRight] range:NSMakeRange(3, 3)]; + + XCTAssertEqual(nilString.qmui_textAlignment, NSTextAlignmentLeft); + XCTAssertEqual(emptyString.qmui_textAlignment, NSTextAlignmentLeft); + XCTAssertEqual(emptyString2.qmui_textAlignment, NSTextAlignmentLeft);// 就算显式写了文本属性,但因为文本长度为0,所以得到的也是默认值 Left + XCTAssertEqual(paraString.qmui_textAlignment, NSTextAlignmentCenter); + XCTAssertEqual(nonParaString.qmui_textAlignment, NSTextAlignmentLeft); + XCTAssertEqual(multiParaString.qmui_textAlignment, NSTextAlignmentCenter);// 子字符串拥有不同段落属性的,取第一个子字符串的段落属性的值 + + + NSMutableAttributedString *paraString2 = (NSMutableAttributedString *)paraString.mutableCopy; + paraString2.qmui_textAlignment = NSTextAlignmentRight; + XCTAssertEqual(paraString2.qmui_textAlignment, NSTextAlignmentRight); + + multiParaString.qmui_textAlignment = NSTextAlignmentRight; + XCTAssertEqual(multiParaString.qmui_textAlignment, NSTextAlignmentRight); +} + +@end diff --git a/QMUI/QMUIKitTests/UIKitExtensions/UIButtonTests.m b/QMUI/QMUIKitTests/UIKitExtensions/UIButtonTests.m new file mode 100644 index 00000000..1af5f892 --- /dev/null +++ b/QMUI/QMUIKitTests/UIKitExtensions/UIButtonTests.m @@ -0,0 +1,124 @@ +/** + * 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. + */ +// +// UIButtonTests.m +// QMUIKitTests +// +// Created by MoLice on 2021/6/15. +// + +#import +#import + +@interface UIButtonTests : XCTestCase + +@end + +@implementation UIButtonTests + +#pragma mark - TitleAttributes + +/** + 1. 两者的存储互不影响,设置了 attributedTitle 后从 title 获取纯文本,得到的依然是 nil。 + 2. attributedTitle 的优先级一定比 title 高,即便 setTitle 更晚设置。 + 3. 当某个 state 没设置值时,会从 normal 取值,不管是 title 还是 attributedTitle。 + 4. 展示逻辑总是优先询问 attributedTitleForState:,再询问 titleForState:,遇到前者有值则用前者。由于第2、3点,假设你设置了 Normal title,再设置了 Highlighted attributedTitle,则都会生效。但如果设置 Normal attirbutedTitle,再设置 Highlighted title,则后者不生效,因为处于 Highlighted 状态时,会先询问 attirubtedTitleForState:Highlighted,此时没有,于是从 attributedTitleForState:Normal 取值,发现有值,则用它,不管你的 Highlighted title 其实是有值的。 + */ + +// 先设置 title,再设置 titleAttributes +- (void)testTitleAttributes1 { + UIButton *button = [[UIButton alloc] init]; + [button setTitle:@"Normal" forState:UIControlStateNormal]; + [button qmui_setTitleAttributes:@{ + NSForegroundColorAttributeName: UIColorRed, + } forState:UIControlStateNormal]; + NSString *title = button.currentTitle; + NSAttributedString *attributedTitle = button.currentAttributedTitle; + XCTAssertTrue([title isEqualToString:attributedTitle.string]); + + [button sizeToFit]; + [button layoutIfNeeded]; + XCTAssertTrue([button.titleLabel.textColor isEqual:UIColorRed]); +} + +// 先设置多个 state 的 title,再设置 titleAttributes +- (void)testTitleAttributes2 { + UIButton *button = [[UIButton alloc] init]; + [button setTitle:@"Normal" forState:UIControlStateNormal]; + [button setTitle:@"Disabled" forState:UIControlStateDisabled]; + [button qmui_setTitleAttributes:@{ + NSForegroundColorAttributeName: UIColorRed, + } forState:UIControlStateNormal]; + XCTAssertTrue([button.currentAttributedTitle.string isEqualToString:@"Normal"]); + + button.enabled = NO; + [button sizeToFit]; + [button layoutIfNeeded]; + XCTAssertTrue([button.currentAttributedTitle.string isEqualToString:@"Disabled"]); + XCTAssertTrue([button.titleLabel.textColor isEqual:UIColorRed]); +} + +// 先设置 titleAttributes,再设置 title +- (void)testTitleAttributes3 { + UIButton *button = [[UIButton alloc] init]; + [button qmui_setTitleAttributes:@{ + NSForegroundColorAttributeName: UIColorRed, + } forState:UIControlStateNormal]; + [button setTitle:@"Normal" forState:UIControlStateNormal]; + + NSString *title = button.currentTitle; + NSAttributedString *attributedTitle = button.currentAttributedTitle; + XCTAssertTrue([title isEqualToString:attributedTitle.string]); + + [button sizeToFit]; + [button layoutIfNeeded]; + XCTAssertTrue([button.titleLabel.textColor isEqual:UIColorRed]); +} + +// 先设置 titleAttributes,再设置多个 state 的 title +- (void)testTitleAttributes4 { + UIButton *button = [[UIButton alloc] init]; + [button qmui_setTitleAttributes:@{ + NSForegroundColorAttributeName: UIColorRed, + } forState:UIControlStateNormal]; + [button setTitle:@"Normal" forState:UIControlStateNormal]; + [button setTitle:@"Disabled" forState:UIControlStateDisabled]; + + XCTAssertTrue([button.currentAttributedTitle.string isEqualToString:@"Normal"]); + + button.enabled = NO; + [button sizeToFit]; + [button layoutIfNeeded]; + XCTAssertTrue([button.currentAttributedTitle.string isEqualToString:@"Disabled"]); + XCTAssertTrue([button.titleLabel.textColor isEqual:UIColorRed]); +} + +// 分别设置多个 state 的 titleAttributes、title +- (void)testTitleAttributes5 { + UIButton *button = [[UIButton alloc] init]; + [button qmui_setTitleAttributes:@{ + NSFontAttributeName: UIFontBoldMake(20), + NSForegroundColorAttributeName: UIColorRed, + } forState:UIControlStateNormal]; + [button qmui_setTitleAttributes:@{ + NSForegroundColorAttributeName: UIColorBlue, + } forState:UIControlStateDisabled]; + [button setTitle:@"Normal" forState:UIControlStateNormal]; + [button setTitle:@"Disabled" forState:UIControlStateDisabled]; + [button sizeToFit]; + [button layoutIfNeeded]; + XCTAssertTrue([button.titleLabel.textColor isEqual:UIColorRed]); + + button.enabled = NO; + XCTAssertTrue([button.titleLabel.textColor isEqual:UIColorBlue]); + + // 自动从 Normal 复制其他样式过来 + XCTAssertTrue(button.titleLabel.font.pointSize == 20); +} + +@end diff --git a/QMUI/QMUIKitTests/UIKitExtensions/UIColorTests.m b/QMUI/QMUIKitTests/UIKitExtensions/UIColorTests.m new file mode 100644 index 00000000..06d5a514 --- /dev/null +++ b/QMUI/QMUIKitTests/UIKitExtensions/UIColorTests.m @@ -0,0 +1,213 @@ +/** + * 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. + */ +// +// UIColorTests.m +// QMUIKitTests +// +// Created by MoLice on 2019/M/15. +// + +#import +#import + +@interface UIColorTests : XCTestCase + +@end + +@implementation UIColorTests + +- (void)testColorWithHexString { + XCTAssertTrue([UIColor qmui_colorWithHexString:@"#f0f"]); + XCTAssertTrue([UIColor qmui_colorWithHexString:@"#F0F"]); + XCTAssertTrue([UIColor qmui_colorWithHexString:@"#0f0f"]); + XCTAssertTrue([UIColor qmui_colorWithHexString:@"#ff00ff"]); + XCTAssertTrue([UIColor qmui_colorWithHexString:@"#00ff00ff"]); + XCTAssertTrue([UIColor qmui_colorWithHexString:@"00ff00ff"]); + XCTAssertFalse([UIColor qmui_colorWithHexString:@""]); + XCTAssertFalse([UIColor qmui_colorWithHexString:nil]); + XCTAssertThrows([UIColor qmui_colorWithHexString:@"#f0f0f"]); +} + +- (void)testHexString { + // 不同颜色空间的 UIColor 对象 + XCTAssertTrue([UIColor colorWithRed:1 green:.5 blue:0 alpha:1].qmui_hexString); + XCTAssertTrue([UIColor colorWithHue:1 saturation:1 brightness:1 alpha:1].qmui_hexString); + XCTAssertTrue([UIColor whiteColor].qmui_hexString); + + UIColor *nilColor = nil; + XCTAssertFalse(nilColor.qmui_hexString); + + NSString *hexString = @"#00ff00ff"; + XCTAssertEqualObjects(hexString, [UIColor qmui_colorWithHexString:hexString].qmui_hexString); +} + +- (void)testRGBA { + // 不同颜色空间的 UIColor 对象 + XCTAssertEqual([UIColor redColor].qmui_red, 1); + XCTAssertEqual([UIColor greenColor].qmui_green, 1); + XCTAssertEqual([UIColor blueColor].qmui_blue, 1); + XCTAssertEqual([UIColor blueColor].qmui_alpha, 1); + + XCTAssertEqualObjects([UIColor redColor].qmui_RGBAString, @"255,0,0,1.00"); + XCTAssertEqualObjects([UIColor greenColor].qmui_RGBAString, @"0,255,0,1.00"); + XCTAssertEqualObjects([UIColor blueColor].qmui_RGBAString, @"0,0,255,1.00"); + + UIColor *graySpaceColor = [UIColor whiteColor]; + XCTAssertEqual(graySpaceColor.qmui_red, 1); + XCTAssertEqual(graySpaceColor.qmui_green, 1); + XCTAssertEqual(graySpaceColor.qmui_blue, 1); + XCTAssertEqual(graySpaceColor.qmui_alpha, 1); + XCTAssertEqualObjects(graySpaceColor.qmui_RGBAString, @"255,255,255,1.00"); + + XCTAssertEqualObjects([UIColor colorWithWhite:1 alpha:.5].qmui_RGBAString, @"255,255,255,0.50"); + + UIColor *hsbSpaceColor = [UIColor colorWithHue:1 saturation:1 brightness:1 alpha:1];// 纯红色 + XCTAssertEqual(hsbSpaceColor.qmui_red, 1); + XCTAssertEqual(hsbSpaceColor.qmui_green, 0); + XCTAssertEqual(hsbSpaceColor.qmui_blue, 0); + XCTAssertEqual(hsbSpaceColor.qmui_alpha, 1); + XCTAssertEqualObjects(hsbSpaceColor.qmui_RGBAString, @"255,0,0,1.00"); + + UIColor *zeroColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0]; + XCTAssertEqual(zeroColor.qmui_red, 0); + XCTAssertEqual(zeroColor.qmui_green, 0); + XCTAssertEqual(zeroColor.qmui_blue, 0); + XCTAssertEqual(zeroColor.qmui_alpha, 0); + XCTAssertEqualObjects(zeroColor.qmui_RGBAString, @"0,0,0,0.00"); + + CGFloat value = .25; + UIColor *nonZeroColor = [UIColor colorWithRed:value green:value blue:value alpha:value]; + XCTAssertEqual(nonZeroColor.qmui_red, value); + XCTAssertEqual(nonZeroColor.qmui_green, value); + XCTAssertEqual(nonZeroColor.qmui_blue, value); + XCTAssertEqual(nonZeroColor.qmui_alpha, value); + XCTAssertEqualObjects(nonZeroColor.qmui_RGBAString, @"64,64,64,0.25"); + + UIColor *nilColor = nil; + XCTAssertEqual(nilColor.qmui_red, 0); + XCTAssertEqual(nilColor.qmui_green, 0); + XCTAssertEqual(nilColor.qmui_blue, 0); + XCTAssertEqual(nilColor.qmui_alpha, 0); + + XCTAssertEqualObjects(UIColorMakeWithRGBA(255, 0, 0, .5), [UIColor qmui_colorWithRGBAString:@"255,0,0,.5"]); +} + +- (void)testHSB { + UIColor *zeroHSBColor = [UIColor colorWithHue:0 saturation:0 brightness:0 alpha:0]; + XCTAssertTrue(zeroHSBColor.qmui_hue == 0 || zeroHSBColor.qmui_hue == 1); + XCTAssertEqual(zeroHSBColor.qmui_saturation, 0); + XCTAssertEqual(zeroHSBColor.qmui_brightness, 0); + XCTAssertEqual(zeroHSBColor.qmui_alpha, 0); + + UIColor *nonZeroHSBColor = [UIColor colorWithHue:1 saturation:1 brightness:1 alpha:1]; + XCTAssertTrue(nonZeroHSBColor.qmui_hue == 0 || nonZeroHSBColor.qmui_hue == 1); + XCTAssertEqual(nonZeroHSBColor.qmui_saturation, 1); + XCTAssertEqual(nonZeroHSBColor.qmui_brightness, 1); + XCTAssertEqual(nonZeroHSBColor.qmui_alpha, 1); + + UIColor *rgbSpaceColor = [UIColor colorWithRed:1 green:0 blue:0 alpha:1]; + XCTAssertTrue(rgbSpaceColor.qmui_hue == 0 || nonZeroHSBColor.qmui_hue == 1); + XCTAssertEqual(rgbSpaceColor.qmui_saturation, 1); + XCTAssertEqual(rgbSpaceColor.qmui_brightness, 1); + XCTAssertEqual(rgbSpaceColor.qmui_alpha, 1); + + UIColor *graySpaceColor = [UIColor whiteColor]; + XCTAssertTrue(graySpaceColor.qmui_hue == 0 || nonZeroHSBColor.qmui_hue == 1); + XCTAssertEqual(graySpaceColor.qmui_saturation, 0); + XCTAssertEqual(graySpaceColor.qmui_brightness, 1); + XCTAssertEqual(graySpaceColor.qmui_alpha, 1); + + UIColor *nilColor = nil; + XCTAssertEqual(nilColor.qmui_hue, 0); + XCTAssertEqual(nilColor.qmui_saturation, 0); + XCTAssertEqual(nilColor.qmui_brightness, 0); + XCTAssertEqual(nilColor.qmui_alpha, 0); +} + +- (void)testColorWithoutAlpha { + UIColor *rgbSpaceColor = [UIColor colorWithRed:1 green:0 blue:0 alpha:.5]; + UIColor *rgbSpaceWithoutAlphaColor = rgbSpaceColor.qmui_colorWithoutAlpha; + XCTAssertTrue(rgbSpaceColor.qmui_red == rgbSpaceWithoutAlphaColor.qmui_red); + XCTAssertTrue(rgbSpaceColor.qmui_green == rgbSpaceWithoutAlphaColor.qmui_green); + XCTAssertTrue(rgbSpaceColor.qmui_blue == rgbSpaceWithoutAlphaColor.qmui_blue); + XCTAssertFalse(rgbSpaceColor.qmui_alpha == rgbSpaceWithoutAlphaColor.qmui_alpha); + + UIColor *graySpaceColor = [[UIColor whiteColor] colorWithAlphaComponent:.5]; + UIColor *graySpaceWithoutAlphaColor = graySpaceColor.qmui_colorWithoutAlpha; + XCTAssertTrue(graySpaceColor.qmui_red == graySpaceWithoutAlphaColor.qmui_red); + XCTAssertTrue(graySpaceColor.qmui_green == graySpaceWithoutAlphaColor.qmui_green); + XCTAssertTrue(graySpaceColor.qmui_blue == graySpaceWithoutAlphaColor.qmui_blue); + XCTAssertFalse(graySpaceColor.qmui_alpha == graySpaceWithoutAlphaColor.qmui_alpha); + + UIColor *nilColor = nil; + XCTAssertFalse(nilColor.qmui_colorWithoutAlpha); +} + +- (void)testColorIsDark { + XCTAssertTrue([UIColor blackColor].qmui_colorIsDark); + XCTAssertTrue([UIColor redColor].qmui_colorIsDark); + XCTAssertFalse([UIColor whiteColor].qmui_colorIsDark); +} + +- (void)testInverseColor { + UIColor *targetColor = [UIColor colorWithRed:1 green:0 blue:0 alpha:.5]; + UIColor *inverseColor = [UIColor colorWithRed:0 green:1 blue:1 alpha:.5]; + XCTAssertEqualObjects(targetColor.qmui_inverseColor, inverseColor); +} + +- (void)testSystemTintColor { + XCTAssertTrue([UIView new].tintColor.qmui_isSystemTintColor); + XCTAssertFalse([UIColor redColor].qmui_isSystemTintColor); +} + +- (void)testColorWithBackendAndFront { + // 前景色不透明则叠加后就是前景色 + XCTAssertEqualObjects([UIColor qmui_colorWithBackendColor:[UIColor redColor] frontColor:[UIColor blackColor]], [UIColor colorWithRed:0 green:0 blue:0 alpha:1]); + + // 前景色半透明则叠加后与前景色不同 + XCTAssertNotEqualObjects([UIColor qmui_colorWithBackendColor:[UIColor redColor] frontColor:[[UIColor blackColor] colorWithAlphaComponent:.5]], [UIColor colorWithRed:0 green:0 blue:0 alpha:1]); + + // 前景色全透明则叠加后与背景色相同 + XCTAssertEqualObjects([UIColor qmui_colorWithBackendColor:[UIColor redColor] frontColor:[UIColor clearColor]], [UIColor colorWithRed:1 green:0 blue:0 alpha:1]); + + // 背景色全透明则叠加后就是前景色 + XCTAssertEqualObjects([UIColor qmui_colorWithBackendColor:[UIColor clearColor] frontColor:[UIColor redColor]], [UIColor colorWithRed:1 green:0 blue:0 alpha:1]); +} + +- (void)testColorFromTo { + XCTAssertEqualObjects([UIColor qmui_colorFromColor:[UIColor blackColor] toColor:[[UIColor blackColor] colorWithAlphaComponent:0] progress:.5], [UIColor colorWithRed:0 green:0 blue:0 alpha:.5]); +} + +- (void)testDistanceOfColors { + UIColor *white = [UIColor colorWithRed:1 green:1 blue:1 alpha:1]; + + // 检测不同色彩空间是否能进行比较 + XCTAssertTrue([white qmui_distanceBetweenColor:UIColor.whiteColor] == 0); + + // 检测同一个对象是否相等 + XCTAssertTrue([white qmui_distanceBetweenColor:[UIColor colorWithRed:1 green:1 blue:1 alpha:1]] == 0); + + CGFloat whiteAndGray = [white qmui_distanceBetweenColor:[UIColor colorWithRed:225.0/255.0 green:225.0/255.0 blue:225.0/255.0 alpha:1]]; + CGFloat whiteAndBlack = [white qmui_distanceBetweenColor:UIColor.blackColor]; + XCTAssertTrue(whiteAndGray > 0); + XCTAssertTrue(whiteAndBlack > 0); + // 灰色应该比纯黑更接近白色 + XCTAssertTrue(whiteAndGray < whiteAndBlack); + + // 测试反色 + UIColor *red = [UIColor colorWithRed:1 green:0 blue:0 alpha:1]; + UIColor *blue = [UIColor colorWithRed:0 green:1 blue:1 alpha:1]; + CGFloat redAndBlue = [red qmui_distanceBetweenColor:blue]; + XCTAssertTrue(redAndBlue > 0); +} + +- (void)testRadomColor { + XCTAssertNotEqualObjects([UIColor qmui_randomColor], [UIColor qmui_randomColor]); +} + +@end diff --git a/QMUI/README.md b/QMUI/README.md index 22b61ed3..fb1a1ff5 100644 --- a/QMUI/README.md +++ b/QMUI/README.md @@ -1,17 +1,15 @@ - -

- Banner -

- # QMUI iOS +

+ Banner +

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.") +开发者:深圳市腾讯计算机系统有限公司 + ## 功能特性 ### 全局 UI 配置 @@ -31,27 +29,43 @@ QMUI iOS 是一个致力于提高项目 UI 开发效率的解决方案,其设 ## 支持iOS版本 -从 3.0.0 开始,QMUI 支持 iOS 9+,2.x 版本支持 iOS 8+。 +1. 4.6.1 及以上,iOS 13+。 +2. 4.4.0 及以上,iOS 11+。 +3. 4.2.0 及以上,iOS 10+。 +4. 3.0.0 及以上,iOS 9+。 +5. 2.0.0 及以上,iOS 8+。 ## 使用方法 -请查看官网的[开始使用](http://qmuiteam.com/ios/page/start.html)。 +``` +pod 'QMUIKit' +``` ## 代码示例 请下载 QMUI Demo:[https://github.com/QMUI/QMUIDemo_iOS](https://github.com/QMUI/QMUIDemo_iOS)。 ![Launch](https://user-images.githubusercontent.com/1190261/49869307-041fdf00-fe4b-11e8-8f77-8007317e71c6.gif) -![UIColor](https://user-images.githubusercontent.com/1190261/49869346-1d289000-fe4b-11e8-9e75-00b861f72b2a.gif) -![Popup](https://user-images.githubusercontent.com/1190261/49869336-169a1880-fe4b-11e8-9fab-b3ff8233d562.gif) -![MarqueeLabel](https://user-images.githubusercontent.com/1190261/49869323-100ba100-fe4b-11e8-947c-92082fb4ddd8.gif) +![QMUITheme](https://user-images.githubusercontent.com/1190261/66378391-ecbb6f00-e9e5-11e9-9d47-8456347ba886.gif) +![QMUIPopup](https://user-images.githubusercontent.com/1190261/49869336-169a1880-fe4b-11e8-9fab-b3ff8233d562.gif) +![QMUIMarqueeLabel](https://user-images.githubusercontent.com/1190261/49869323-100ba100-fe4b-11e8-947c-92082fb4ddd8.gif) ## 注意事项 - 关于 AutoLayout:通常可以配合 Masonry 等常见的 AutoLayout 框架使用,若遇到不兼容的个案请提 issue。 - 关于 xib / storyboard:现已全面支持。 - 关于 Swift:可以正常使用,如遇到问题请提 issue。 -- 关于隐私:从 2.8.0 版本开始,QMUIKit 默认会在 Debug 模式下启动 App 时发送当前 App 的 Bundle Identifier 和 Display Name 给 QMUI 作统计用,Release 下不会发送。你也可以通过配置表的 `SendAnalyticsToQMUITeam` 开关将统计关闭。统计的代码在 [QMUIConfiguration.m:91](https://github.com/Tencent/QMUI_iOS/blob/master/QMUIKit/QMUICore/QMUIConfiguration.m#L91-L101),可直接查看。 +- 关于 UIScene:暂不支持 Multiple Window。 + +## 隐私政策 + +如果你想了解使用 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)。 + +## 设计资源 + +QMUIKit 框架内自带图片资源的组件主要是 QMUIConsole、QMUIEmotion、QMUIImagePicker、QMUITips,另外作为 Sample Code 使用的 QMUI Demo 是另一个独立的项目,它拥有自己另外一套设计。 + +QMUIKit 和 QMUI Demo 的 Sketch 设计稿均存放在 [https://github.com/QMUI/QMUIDemo_Design](https://github.com/QMUI/QMUIDemo_Design)。 ## 其他 diff --git a/QMUI/new_license_content.txt b/QMUI/new_license_content.txt index b47d0efb..be2085b1 100644 --- a/QMUI/new_license_content.txt +++ b/QMUI/new_license_content.txt @@ -1,7 +1,7 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ diff --git a/QMUI/old_license_content.txt b/QMUI/old_license_content.txt index b47d0efb..be2085b1 100644 --- a/QMUI/old_license_content.txt +++ b/QMUI/old_license_content.txt @@ -1,7 +1,7 @@ -/***** +/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ diff --git a/QMUI/qmui.xcodeproj/project.pbxproj b/QMUI/qmui.xcodeproj/project.pbxproj index 35c8c63e..9381ac23 100644 --- a/QMUI/qmui.xcodeproj/project.pbxproj +++ b/QMUI/qmui.xcodeproj/project.pbxproj @@ -3,12 +3,19 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ + 08230CEC233D285B00BF9CB1 /* UISearchController+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 08230CEA233D285B00BF9CB1 /* UISearchController+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 08230CED233D285B00BF9CB1 /* UISearchController+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 08230CEB233D285B00BF9CB1 /* UISearchController+QMUI.m */; }; + 083551A92438C0D000B8FEAB /* CALayer+QMUIViewAnimation.h in Headers */ = {isa = PBXBuildFile; fileRef = 083551A72438C0D000B8FEAB /* CALayer+QMUIViewAnimation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 083551AA2438C0D000B8FEAB /* CALayer+QMUIViewAnimation.m in Sources */ = {isa = PBXBuildFile; fileRef = 083551A82438C0D000B8FEAB /* CALayer+QMUIViewAnimation.m */; }; + 08B399C922E18A3B000A8A45 /* UITraitCollection+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 08B399C722E18A3B000A8A45 /* UITraitCollection+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 08B399CA22E18A3B000A8A45 /* UITraitCollection+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 08B399C822E18A3B000A8A45 /* UITraitCollection+QMUI.m */; }; 1178D5692198258700AA30E5 /* NSURL+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 1178D5672198258700AA30E5 /* NSURL+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1178D56A2198258700AA30E5 /* NSURL+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 1178D5682198258700AA30E5 /* NSURL+QMUI.m */; }; + 3CB960C42BB40725005626A6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3CB960C32BB40725005626A6 /* PrivacyInfo.xcprivacy */; }; AA8860BA2107455C005E4054 /* QMUIWeakObjectContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = AA8860B82107455C005E4054 /* QMUIWeakObjectContainer.h */; settings = {ATTRIBUTES = (Public, ); }; }; AA8860BB2107455C005E4054 /* QMUIWeakObjectContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = AA8860B92107455C005E4054 /* QMUIWeakObjectContainer.m */; }; CD046C412018668900092035 /* QMUILogItem.h in Headers */ = {isa = PBXBuildFile; fileRef = CD046C3F2018668900092035 /* QMUILogItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -18,6 +25,10 @@ CD046C492018688F00092035 /* QMUILogger.h in Headers */ = {isa = PBXBuildFile; fileRef = CD046C472018688F00092035 /* QMUILogger.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD046C4A2018688F00092035 /* QMUILogger.m in Sources */ = {isa = PBXBuildFile; fileRef = CD046C482018688F00092035 /* QMUILogger.m */; }; CD046C4D2018698200092035 /* QMUILog.h in Headers */ = {isa = PBXBuildFile; fileRef = CD046C4B2018698200092035 /* QMUILog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD0A1BAA273512D5002A1A54 /* QMUIStringPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = CD0A1BA8273512D5002A1A54 /* QMUIStringPrivate.h */; }; + CD0A1BAB273512D5002A1A54 /* QMUIStringPrivate.m in Sources */ = {isa = PBXBuildFile; fileRef = CD0A1BA9273512D5002A1A54 /* QMUIStringPrivate.m */; }; + CD0BD676233B9888005E47CE /* UIView+QMUITheme.h in Headers */ = {isa = PBXBuildFile; fileRef = 089F1E4A2322F6D50063061E /* UIView+QMUITheme.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD0BD68B234F6C34005E47CE /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CD0BD68A234F6C34005E47CE /* Images.xcassets */; }; CD1817E42010CC4000F8CDEC /* NSNumber+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CD1817E22010CC4000F8CDEC /* NSNumber+QMUI.m */; }; CD1817E52010CC4000F8CDEC /* NSNumber+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CD1817E32010CC4000F8CDEC /* NSNumber+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD18BC7321760D5900E2A4E6 /* QMUINavigationBarScrollingAnimator.h in Headers */ = {isa = PBXBuildFile; fileRef = CD18BC7121760D5900E2A4E6 /* QMUINavigationBarScrollingAnimator.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -25,30 +36,54 @@ CD18CDFE20EE167200EED53C /* UITableViewCell+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CD18CDFC20EE167200EED53C /* UITableViewCell+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD18CDFF20EE167200EED53C /* UITableViewCell+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CD18CDFD20EE167200EED53C /* UITableViewCell+QMUI.m */; }; CD19F4D821E4AB3900BD4687 /* QMUILab.h in Headers */ = {isa = PBXBuildFile; fileRef = CD19F4D721E4AB3900BD4687 /* QMUILab.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD2B19712A715D6200E8ED18 /* QMUIBadgeLabel.h in Headers */ = {isa = PBXBuildFile; fileRef = CD2B196F2A715D6200E8ED18 /* QMUIBadgeLabel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD2B19722A715D6200E8ED18 /* QMUIBadgeLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = CD2B19702A715D6200E8ED18 /* QMUIBadgeLabel.m */; }; CD349BAD2160AF75008653D4 /* QMUIScrollAnimator.h in Headers */ = {isa = PBXBuildFile; fileRef = CD349BAB2160AF75008653D4 /* QMUIScrollAnimator.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD349BAE2160AF75008653D4 /* QMUIScrollAnimator.m in Sources */ = {isa = PBXBuildFile; fileRef = CD349BAC2160AF75008653D4 /* QMUIScrollAnimator.m */; }; CD349BB72160B83D008653D4 /* QMUINavigationBarScrollingSnapAnimator.h in Headers */ = {isa = PBXBuildFile; fileRef = CD349BB52160B83D008653D4 /* QMUINavigationBarScrollingSnapAnimator.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD349BB82160B83D008653D4 /* QMUINavigationBarScrollingSnapAnimator.m in Sources */ = {isa = PBXBuildFile; fileRef = CD349BB62160B83D008653D4 /* QMUINavigationBarScrollingSnapAnimator.m */; }; + CD40021B2C1F6BB0003D2127 /* QMUIPopupMenuItem.m in Sources */ = {isa = PBXBuildFile; fileRef = CD40021A2C1F6BB0003D2127 /* QMUIPopupMenuItem.m */; }; + CD40021C2C1F6BB0003D2127 /* QMUIPopupMenuItem.h in Headers */ = {isa = PBXBuildFile; fileRef = CD4002192C1F6BB0003D2127 /* QMUIPopupMenuItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD40021E2C1F6E1C003D2127 /* QMUIPopupMenuItemViewProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = CD40021D2C1F6E1C003D2127 /* QMUIPopupMenuItemViewProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD4002212C1F81CE003D2127 /* QMUIPopupMenuItemView.m in Sources */ = {isa = PBXBuildFile; fileRef = CD4002202C1F81CE003D2127 /* QMUIPopupMenuItemView.m */; }; + CD4002222C1F81CE003D2127 /* QMUIPopupMenuItemView.h in Headers */ = {isa = PBXBuildFile; fileRef = CD40021F2C1F81CE003D2127 /* QMUIPopupMenuItemView.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD43CB17207B98A10090346B /* QMUIButton.h in Headers */ = {isa = PBXBuildFile; fileRef = CD43CB15207B98A10090346B /* QMUIButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD43CB18207B98A10090346B /* QMUIButton.m in Sources */ = {isa = PBXBuildFile; fileRef = CD43CB16207B98A10090346B /* QMUIButton.m */; }; CD43CB1B207B98B60090346B /* QMUINavigationButton.h in Headers */ = {isa = PBXBuildFile; fileRef = CD43CB19207B98B60090346B /* QMUINavigationButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD43CB1C207B98B60090346B /* QMUINavigationButton.m in Sources */ = {isa = PBXBuildFile; fileRef = CD43CB1A207B98B60090346B /* QMUINavigationButton.m */; }; CD43CB1F207B9A510090346B /* QMUIToolbarButton.h in Headers */ = {isa = PBXBuildFile; fileRef = CD43CB1D207B9A510090346B /* QMUIToolbarButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD43CB20207B9A510090346B /* QMUIToolbarButton.m in Sources */ = {isa = PBXBuildFile; fileRef = CD43CB1E207B9A510090346B /* QMUIToolbarButton.m */; }; - CD43CB23207B9ACA0090346B /* QMUILinkButton.h in Headers */ = {isa = PBXBuildFile; fileRef = CD43CB21207B9ACA0090346B /* QMUILinkButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD43CB24207B9ACA0090346B /* QMUILinkButton.m in Sources */ = {isa = PBXBuildFile; fileRef = CD43CB22207B9ACA0090346B /* QMUILinkButton.m */; }; - CD43CB27207B9B1A0090346B /* QMUIGhostButton.h in Headers */ = {isa = PBXBuildFile; fileRef = CD43CB25207B9B1A0090346B /* QMUIGhostButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD43CB28207B9B1A0090346B /* QMUIGhostButton.m in Sources */ = {isa = PBXBuildFile; fileRef = CD43CB26207B9B1A0090346B /* QMUIGhostButton.m */; }; - CD43CB2B207B9B840090346B /* QMUIFillButton.h in Headers */ = {isa = PBXBuildFile; fileRef = CD43CB29207B9B840090346B /* QMUIFillButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD43CB2C207B9B840090346B /* QMUIFillButton.m in Sources */ = {isa = PBXBuildFile; fileRef = CD43CB2A207B9B840090346B /* QMUIFillButton.m */; }; CD4EA4BF2275FA0100A55066 /* NSMethodSignature+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CD4EA4BD2275FA0100A55066 /* NSMethodSignature+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD4EA4C02275FA0100A55066 /* NSMethodSignature+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CD4EA4BE2275FA0100A55066 /* NSMethodSignature+QMUI.m */; }; + CD4EA576228C401E00A55066 /* QMUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE0AFAD11D82B9D8000D21D9 /* QMUIKit.framework */; }; + CD4EA57E228C443B00A55066 /* UIColorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CD4EA57D228C443B00A55066 /* UIColorTests.m */; }; + CD513E28283527AA004A549D /* QMUIBarProtocolPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = CD513E25283527AA004A549D /* QMUIBarProtocolPrivate.h */; }; + CD513E29283527AA004A549D /* QMUIBarProtocolPrivate.m in Sources */ = {isa = PBXBuildFile; fileRef = CD513E26283527AA004A549D /* QMUIBarProtocolPrivate.m */; }; + CD513E2A283527AA004A549D /* QMUIBarProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = CD513E27283527AA004A549D /* QMUIBarProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD513E2D283527CE004A549D /* UITabBar+QMUIBarProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = CD513E2B283527CE004A549D /* UITabBar+QMUIBarProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD513E2E283527CE004A549D /* UITabBar+QMUIBarProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = CD513E2C283527CE004A549D /* UITabBar+QMUIBarProtocol.m */; }; + CD513E31283527DB004A549D /* UINavigationBar+QMUIBarProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = CD513E2F283527DB004A549D /* UINavigationBar+QMUIBarProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD513E32283527DB004A549D /* UINavigationBar+QMUIBarProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = CD513E30283527DB004A549D /* UINavigationBar+QMUIBarProtocol.m */; }; + CD5E43212B85F7200030CFDA /* NSRegularExpression+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CD5E431F2B85F71F0030CFDA /* NSRegularExpression+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD5E43222B85F7200030CFDA /* NSRegularExpression+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CD5E43202B85F71F0030CFDA /* NSRegularExpression+QMUI.m */; }; + CD60DB512C5BC5D1005109B3 /* QMUICheckbox.h in Headers */ = {isa = PBXBuildFile; fileRef = CD60DB4F2C5BC5D1005109B3 /* QMUICheckbox.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD60DB522C5BC5D1005109B3 /* QMUICheckbox.m in Sources */ = {isa = PBXBuildFile; fileRef = CD60DB502C5BC5D1005109B3 /* QMUICheckbox.m */; }; CD6631DB1FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.h in Headers */ = {isa = PBXBuildFile; fileRef = CD6631D91FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD6631DC1FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.m in Sources */ = {isa = PBXBuildFile; fileRef = CD6631DA1FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.m */; }; + CD669A0D25F79DA40036D6B2 /* UICollectionViewCell+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CD669A0B25F79DA40036D6B2 /* UICollectionViewCell+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD669A0E25F79DA40036D6B2 /* UICollectionViewCell+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CD669A0C25F79DA40036D6B2 /* UICollectionViewCell+QMUI.m */; }; CD6BE14E2058C64E00BE093E /* QMUICellHeightKeyCache.h in Headers */ = {isa = PBXBuildFile; fileRef = CD6BE14C2058C64E00BE093E /* QMUICellHeightKeyCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD6BE14F2058C64E00BE093E /* QMUICellHeightKeyCache.m in Sources */ = {isa = PBXBuildFile; fileRef = CD6BE14D2058C64E00BE093E /* QMUICellHeightKeyCache.m */; }; CD6BE1562058C73600BE093E /* UITableView+QMUICellHeightKeyCache.h in Headers */ = {isa = PBXBuildFile; fileRef = CD6BE1542058C73600BE093E /* UITableView+QMUICellHeightKeyCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD6BE1572058C73600BE093E /* UITableView+QMUICellHeightKeyCache.m in Sources */ = {isa = PBXBuildFile; fileRef = CD6BE1552058C73600BE093E /* UITableView+QMUICellHeightKeyCache.m */; }; + CD70C43A276340B300D212F5 /* UISlider+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CD70C438276340B300D212F5 /* UISlider+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD70C43B276340B300D212F5 /* UISlider+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CD70C439276340B300D212F5 /* UISlider+QMUI.m */; }; + CD72E7C12B440DF000AC528A /* QMUILayouterItem.h in Headers */ = {isa = PBXBuildFile; fileRef = CD72E7BF2B440DF000AC528A /* QMUILayouterItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD72E7C22B440DF000AC528A /* QMUILayouterItem.m in Sources */ = {isa = PBXBuildFile; fileRef = CD72E7C02B440DF000AC528A /* QMUILayouterItem.m */; }; + CD72E7C72B44AF2F00AC528A /* QMUILayouterLinearHorizontal.h in Headers */ = {isa = PBXBuildFile; fileRef = CD72E7C52B44AF2F00AC528A /* QMUILayouterLinearHorizontal.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD72E7C82B44AF2F00AC528A /* QMUILayouterLinearHorizontal.m in Sources */ = {isa = PBXBuildFile; fileRef = CD72E7C62B44AF2F00AC528A /* QMUILayouterLinearHorizontal.m */; }; + CD72E7CB2B44AF8800AC528A /* QMUILayouterLinearVertical.h in Headers */ = {isa = PBXBuildFile; fileRef = CD72E7C92B44AF8800AC528A /* QMUILayouterLinearVertical.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD72E7CC2B44AF8800AC528A /* QMUILayouterLinearVertical.m in Sources */ = {isa = PBXBuildFile; fileRef = CD72E7CA2B44AF8800AC528A /* QMUILayouterLinearVertical.m */; }; CD745E2C21CA5B8F006EC132 /* QMUIImagePreviewView.h in Headers */ = {isa = PBXBuildFile; fileRef = CD745E2821CA5B8E006EC132 /* QMUIImagePreviewView.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD745E2D21CA5B8F006EC132 /* QMUIImagePreviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD745E2921CA5B8E006EC132 /* QMUIImagePreviewViewController.m */; }; CD745E2E21CA5B8F006EC132 /* QMUIImagePreviewViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CD745E2A21CA5B8E006EC132 /* QMUIImagePreviewViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -57,6 +92,11 @@ CD745E3321CA5BBB006EC132 /* QMUIImagePreviewViewTransitionAnimator.m in Sources */ = {isa = PBXBuildFile; fileRef = CD745E3121CA5BBB006EC132 /* QMUIImagePreviewViewTransitionAnimator.m */; }; CD766F7A216B52F3005155BD /* UINavigationBar+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CD766F78216B52F3005155BD /* UINavigationBar+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD766F7B216B52F3005155BD /* UINavigationBar+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CD766F79216B52F3005155BD /* UINavigationBar+QMUI.m */; }; + 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, ); }; }; @@ -71,13 +111,23 @@ CD8AA7B421E8C0F300BA7369 /* QMUIConsoleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD8AA7B221E8C0F300BA7369 /* QMUIConsoleViewController.m */; }; CD8AA7C221EDE06800BA7369 /* QMUILog+QMUIConsole.h in Headers */ = {isa = PBXBuildFile; fileRef = CD8AA7C021EDE06800BA7369 /* QMUILog+QMUIConsole.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD8AA7C321EDE06800BA7369 /* QMUILog+QMUIConsole.m in Sources */ = {isa = PBXBuildFile; fileRef = CD8AA7C121EDE06800BA7369 /* QMUILog+QMUIConsole.m */; }; + CD8CB8C222DE10F200B0C9F8 /* UIImage+QMUITheme.h in Headers */ = {isa = PBXBuildFile; fileRef = CD8CB8C022DE10F200B0C9F8 /* UIImage+QMUITheme.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD8CB8C322DE10F200B0C9F8 /* UIImage+QMUITheme.m in Sources */ = {isa = PBXBuildFile; fileRef = CD8CB8C122DE10F200B0C9F8 /* UIImage+QMUITheme.m */; }; + CD96A2B928C74CCA00E87728 /* NSShadow+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CD96A2B728C74CCA00E87728 /* NSShadow+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD96A2BA28C74CCA00E87728 /* NSShadow+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CD96A2B828C74CCA00E87728 /* NSShadow+QMUI.m */; }; CD979996213F934700C00FDC /* QMUIRuntime.m in Sources */ = {isa = PBXBuildFile; fileRef = CD979995213F934700C00FDC /* QMUIRuntime.m */; }; CD9D6E6E210C8DEC0004E222 /* QMUILogger+QMUIConfigurationTemplate.h in Headers */ = {isa = PBXBuildFile; fileRef = CD9D6E6C210C8DEC0004E222 /* QMUILogger+QMUIConfigurationTemplate.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD9D6E6F210C8DEC0004E222 /* QMUILogger+QMUIConfigurationTemplate.m in Sources */ = {isa = PBXBuildFile; fileRef = CD9D6E6D210C8DEC0004E222 /* QMUILogger+QMUIConfigurationTemplate.m */; }; - CD9D6EBC210F8CA20004E222 /* CAAnimation+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CD9D6EBA210F8CA20004E222 /* CAAnimation+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD9D6EBD210F8CA20004E222 /* CAAnimation+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CD9D6EBB210F8CA20004E222 /* CAAnimation+QMUI.m */; }; + CD9F48AA22C3985200F5C5C2 /* QMUIThemePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = CD9F48A822C3985200F5C5C2 /* QMUIThemePrivate.h */; }; + CD9F48AB22C3985200F5C5C2 /* QMUIThemePrivate.m in Sources */ = {isa = PBXBuildFile; fileRef = CD9F48A922C3985200F5C5C2 /* QMUIThemePrivate.m */; }; CDA4083E214F7E2500740888 /* NSCharacterSet+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDA4083C214F7E2500740888 /* NSCharacterSet+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDA4083F214F7E2500740888 /* NSCharacterSet+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA4083D214F7E2500740888 /* NSCharacterSet+QMUI.m */; }; + CDAA653622BBC1240004C6BB /* UIColor+QMUITheme.h in Headers */ = {isa = PBXBuildFile; fileRef = CDAA653422BBC1240004C6BB /* UIColor+QMUITheme.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDAA653722BBC1240004C6BB /* UIColor+QMUITheme.m in Sources */ = {isa = PBXBuildFile; fileRef = CDAA653522BBC1240004C6BB /* UIColor+QMUITheme.m */; }; + CDAA653A22BBC3340004C6BB /* QMUIThemeManager.h in Headers */ = {isa = PBXBuildFile; fileRef = CDAA653822BBC3340004C6BB /* QMUIThemeManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDAA653B22BBC3340004C6BB /* QMUIThemeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CDAA653922BBC3340004C6BB /* QMUIThemeManager.m */; }; + CDAB2D262357481700C96B31 /* UITextInputTraits+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDAB2D242357481700C96B31 /* UITextInputTraits+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDAB2D272357481700C96B31 /* UITextInputTraits+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDAB2D252357481700C96B31 /* UITextInputTraits+QMUI.m */; }; CDB8CACF1DCC870700769DF0 /* QMUIKit.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA2F1DCC870700769DF0 /* QMUIKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDB8CB511DCC870700769DF0 /* CALayer+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA761DCC870700769DF0 /* CALayer+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDB8CB541DCC870700769DF0 /* CALayer+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA771DCC870700769DF0 /* CALayer+QMUI.m */; }; @@ -125,6 +175,7 @@ CDB8CBE01DCC870800769DF0 /* UIViewController+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CABD1DCC870700769DF0 /* UIViewController+QMUI.m */; }; CDB8CBE11DCC870800769DF0 /* UIWindow+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CABE1DCC870700769DF0 /* UIWindow+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDB8CBE41DCC870800769DF0 /* UIWindow+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CABF1DCC870700769DF0 /* UIWindow+QMUI.m */; }; + CDC006E522A804D800A81771 /* NSObjectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC006E422A804D800A81771 /* NSObjectTests.m */; }; CDC163C6204D441000E4CC13 /* QMUILogManagerViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC163C4204D441000E4CC13 /* QMUILogManagerViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDC163C7204D441000E4CC13 /* QMUILogManagerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC163C5204D441000E4CC13 /* QMUILogManagerViewController.m */; }; CDC86FBD1F68D617000E8829 /* QMUIAsset.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F411F68D5F9000E8829 /* QMUIAsset.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -135,7 +186,7 @@ CDC86FC21F68D617000E8829 /* QMUIImagePickerHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F4C1F68D5F9000E8829 /* QMUIImagePickerHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDC86FC31F68D617000E8829 /* QMUIImagePickerPreviewViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F4E1F68D5F9000E8829 /* QMUIImagePickerPreviewViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDC86FC41F68D617000E8829 /* QMUIImagePickerViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F501F68D5F9000E8829 /* QMUIImagePickerViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDC86FC51F68D617000E8829 /* UINavigationBar+Transition.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F531F68D5F9000E8829 /* UINavigationBar+Transition.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FC51F68D617000E8829 /* UINavigationBar+Transition.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F531F68D5F9000E8829 /* UINavigationBar+Transition.h */; }; CDC86FC61F68D617000E8829 /* UINavigationController+NavigationBarTransition.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F551F68D5F9000E8829 /* UINavigationController+NavigationBarTransition.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDC86FC71F68D617000E8829 /* QMUIAlertController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F571F68D5F9000E8829 /* QMUIAlertController.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDC86FCA1F68D617000E8829 /* QMUICollectionViewPagingLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F5D1F68D5F9000E8829 /* QMUICollectionViewPagingLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -157,7 +208,6 @@ CDC86FDD1F68D617000E8829 /* QMUISearchBar.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F831F68D5F9000E8829 /* QMUISearchBar.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDC86FDE1F68D617000E8829 /* QMUISearchController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F851F68D5F9000E8829 /* QMUISearchController.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDC86FDF1F68D617000E8829 /* QMUISegmentedControl.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F871F68D5F9000E8829 /* QMUISegmentedControl.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDC86FE01F68D617000E8829 /* QMUISlider.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F891F68D5F9000E8829 /* QMUISlider.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDC86FE11F68D617000E8829 /* QMUITableView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F8B1F68D5F9000E8829 /* QMUITableView.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDC86FE21F68D617000E8829 /* QMUITableViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F8D1F68D5F9000E8829 /* QMUITableViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDC86FE31F68D617000E8829 /* QMUITableViewProtocols.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F8F1F68D5F9000E8829 /* QMUITableViewProtocols.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -212,7 +262,6 @@ CDC8701A1F68D63B000E8829 /* QMUISearchBar.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F841F68D5F9000E8829 /* QMUISearchBar.m */; }; CDC8701B1F68D63B000E8829 /* QMUISearchController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F861F68D5F9000E8829 /* QMUISearchController.m */; }; CDC8701C1F68D63B000E8829 /* QMUISegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F881F68D5F9000E8829 /* QMUISegmentedControl.m */; }; - CDC8701D1F68D63B000E8829 /* QMUISlider.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F8A1F68D5F9000E8829 /* QMUISlider.m */; }; CDC8701E1F68D63B000E8829 /* QMUITableView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F8C1F68D5F9000E8829 /* QMUITableView.m */; }; CDC8701F1F68D63B000E8829 /* QMUITableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F8E1F68D5F9000E8829 /* QMUITableViewCell.m */; }; CDC870201F68D63B000E8829 /* QMUITestView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F911F68D5F9000E8829 /* QMUITestView.m */; }; @@ -233,46 +282,102 @@ CDC870301F68D63B000E8829 /* QMUICommonViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86FB81F68D5F9000E8829 /* QMUICommonViewController.m */; }; CDC870311F68D63B000E8829 /* QMUINavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86FBA1F68D5F9000E8829 /* QMUINavigationController.m */; }; CDC870321F68D63B000E8829 /* QMUITabBarViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86FBC1F68D5F9000E8829 /* QMUITabBarViewController.m */; }; - CDC870401F68D66A000E8829 /* QMUIResources.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CDC86F3E1F68D5F8000E8829 /* QMUIResources.bundle */; }; + CDCD27032B8E0B6200D3500A /* QMUISheetPresentationSupports.m in Sources */ = {isa = PBXBuildFile; fileRef = CDCD27012B8E0B6200D3500A /* QMUISheetPresentationSupports.m */; }; + CDCD27042B8E0B6200D3500A /* QMUISheetPresentationSupports.h in Headers */ = {isa = PBXBuildFile; fileRef = CDCD27022B8E0B6200D3500A /* QMUISheetPresentationSupports.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDCD27072B8E0C2700D3500A /* QMUISheetPresentationNavigationBar.h in Headers */ = {isa = PBXBuildFile; fileRef = CDCD27052B8E0C2700D3500A /* QMUISheetPresentationNavigationBar.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDCD27082B8E0C2700D3500A /* QMUISheetPresentationNavigationBar.m in Sources */ = {isa = PBXBuildFile; fileRef = CDCD27062B8E0C2700D3500A /* QMUISheetPresentationNavigationBar.m */; }; CDD071FD2060F82700343AB6 /* QMUICellHeightCache.h in Headers */ = {isa = PBXBuildFile; fileRef = CDD071FB2060F82700343AB6 /* QMUICellHeightCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDD071FE2060F82700343AB6 /* QMUICellHeightCache.m in Sources */ = {isa = PBXBuildFile; fileRef = CDD071FC2060F82700343AB6 /* QMUICellHeightCache.m */; }; CDD12D3C1FBB320E00114EA9 /* NSArray+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDD12D3A1FBB320E00114EA9 /* NSArray+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDD12D3D1FBB320E00114EA9 /* NSArray+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDD12D3B1FBB320E00114EA9 /* NSArray+QMUI.m */; }; + CDD7599D22BBE11200BC8F36 /* QMUITheme.h in Headers */ = {isa = PBXBuildFile; fileRef = CDD7599C22BBE11200BC8F36 /* QMUITheme.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDD759A822BBE68900BC8F36 /* CAAnimation+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDD759A622BBE68600BC8F36 /* CAAnimation+QMUI.m */; }; + CDD759A922BBE68900BC8F36 /* CAAnimation+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDD759A722BBE68600BC8F36 /* CAAnimation+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDD7C0D4212300A000D6FA1E /* QMUIRuntime.h in Headers */ = {isa = PBXBuildFile; fileRef = CDD7C0D3212300A000D6FA1E /* QMUIRuntime.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDD7C2B5212C4E0600D6FA1E /* QMUIPopupMenuItemProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = CDD7C2B4212C4E0600D6FA1E /* QMUIPopupMenuItemProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDD7C2B8212C4E7400D6FA1E /* QMUIPopupMenuBaseItem.h in Headers */ = {isa = PBXBuildFile; fileRef = CDD7C2B6212C4E7400D6FA1E /* QMUIPopupMenuBaseItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDD7C2B9212C4E7400D6FA1E /* QMUIPopupMenuBaseItem.m in Sources */ = {isa = PBXBuildFile; fileRef = CDD7C2B7212C4E7400D6FA1E /* QMUIPopupMenuBaseItem.m */; }; - CDD7C2BC212C510B00D6FA1E /* QMUIPopupMenuButtonItem.h in Headers */ = {isa = PBXBuildFile; fileRef = CDD7C2BA212C510B00D6FA1E /* QMUIPopupMenuButtonItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDD7C2BD212C510B00D6FA1E /* QMUIPopupMenuButtonItem.m in Sources */ = {isa = PBXBuildFile; fileRef = CDD7C2BB212C510B00D6FA1E /* QMUIPopupMenuButtonItem.m */; }; CDD7C2C0212C528500D6FA1E /* QMUIPopupMenuView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDD7C2BE212C528400D6FA1E /* QMUIPopupMenuView.m */; }; CDD7C2C1212C528500D6FA1E /* QMUIPopupMenuView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDD7C2BF212C528400D6FA1E /* QMUIPopupMenuView.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDE418FB20761A0F002ED021 /* UIBarItem+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDE418F920761A0F002ED021 /* UIBarItem+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDE418FC20761A0F002ED021 /* UIBarItem+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE418FA20761A0F002ED021 /* UIBarItem+QMUI.m */; }; + CDE77513274E93CE0066A767 /* UIToolbar+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDE77511274E93CE0066A767 /* UIToolbar+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDE77514274E93CE0066A767 /* UIToolbar+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE77512274E93CE0066A767 /* UIToolbar+QMUI.m */; }; + CDE77517274FB9430066A767 /* UIBlurEffect+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDE77515274FB9430066A767 /* UIBlurEffect+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDE77518274FB9430066A767 /* UIBlurEffect+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE77516274FB9430066A767 /* UIBlurEffect+QMUI.m */; }; CDEA6D081F4B07E700F627AF /* UIGestureRecognizer+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDEA6D061F4B07E700F627AF /* UIGestureRecognizer+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDEA6D0A1F4B07E700F627AF /* UIGestureRecognizer+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDEA6D071F4B07E700F627AF /* UIGestureRecognizer+QMUI.m */; }; CDF2D69C207F7E3F009E04DD /* NSPointerArray+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDF2D69A207F7E3F009E04DD /* NSPointerArray+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDF2D69D207F7E3F009E04DD /* NSPointerArray+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDF2D69B207F7E3F009E04DD /* NSPointerArray+QMUI.m */; }; + CDFCDDA02B43FF07005E1219 /* QMUILayouter.h in Headers */ = {isa = PBXBuildFile; fileRef = CDFCDD9E2B43FF07005E1219 /* QMUILayouter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDFE9575293FB1DE007AE1AA /* QMUIKit.podspec in Resources */ = {isa = PBXBuildFile; fileRef = CDFE9574293FB1DE007AE1AA /* QMUIKit.podspec */; }; + CDFF5FB62369926300B63B92 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDFF5FB52369926300B63B92 /* Photos.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + D00881762677B5870061CABF /* UIButtonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D00881752677B5870061CABF /* UIButtonTests.m */; }; + D00B6521242A67D7002C27AB /* QMUIAppearance.h in Headers */ = {isa = PBXBuildFile; fileRef = D00B651F242A67D7002C27AB /* QMUIAppearance.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D00B6522242A67D7002C27AB /* QMUIAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = D00B6520242A67D7002C27AB /* QMUIAppearance.m */; }; + D0193BE822E3449D00FB76F5 /* UIVisualEffect+QMUITheme.h in Headers */ = {isa = PBXBuildFile; fileRef = D0193BE622E3449D00FB76F5 /* UIVisualEffect+QMUITheme.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0193BE922E3449D00FB76F5 /* UIVisualEffect+QMUITheme.m in Sources */ = {isa = PBXBuildFile; fileRef = D0193BE722E3449D00FB76F5 /* UIVisualEffect+QMUITheme.m */; }; + D02096B226DD2B180029BA78 /* UIApplication+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D02096B026DD2B170029BA78 /* UIApplication+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D02096B326DD2B180029BA78 /* UIApplication+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = D02096B126DD2B180029BA78 /* UIApplication+QMUI.m */; }; D021DE37205E809500FFA408 /* UICollectionView+QMUICellSizeKeyCache.h in Headers */ = {isa = PBXBuildFile; fileRef = D021DE35205E809500FFA408 /* UICollectionView+QMUICellSizeKeyCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; D021DE38205E809500FFA408 /* UICollectionView+QMUICellSizeKeyCache.m in Sources */ = {isa = PBXBuildFile; fileRef = D021DE36205E809500FFA408 /* UICollectionView+QMUICellSizeKeyCache.m */; }; D021DE3B205E80EB00FFA408 /* QMUICellSizeKeyCache.h in Headers */ = {isa = PBXBuildFile; fileRef = D021DE39205E80EB00FFA408 /* QMUICellSizeKeyCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; D021DE3C205E80EB00FFA408 /* QMUICellSizeKeyCache.m in Sources */ = {isa = PBXBuildFile; fileRef = D021DE3A205E80EB00FFA408 /* QMUICellSizeKeyCache.m */; }; + D02FDB6E22D880F800DB7E13 /* UISwitch+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D02FDB6C22D880F800DB7E13 /* UISwitch+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D02FDB6F22D880F800DB7E13 /* UISwitch+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = D02FDB6D22D880F800DB7E13 /* UISwitch+QMUI.m */; }; + D03102B524A8CB410095C232 /* UIView+QMUIBorder.h in Headers */ = {isa = PBXBuildFile; fileRef = D03102B324A8CB410095C232 /* UIView+QMUIBorder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D03102B624A8CB410095C232 /* UIView+QMUIBorder.m in Sources */ = {isa = PBXBuildFile; fileRef = D03102B424A8CB410095C232 /* UIView+QMUIBorder.m */; }; + D031843B22C287EA00B43520 /* UIViewController+QMUITheme.h in Headers */ = {isa = PBXBuildFile; fileRef = D031843922C287EA00B43520 /* UIViewController+QMUITheme.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D031843C22C287EA00B43520 /* UIViewController+QMUITheme.m in Sources */ = {isa = PBXBuildFile; fileRef = D031843A22C287EA00B43520 /* UIViewController+QMUITheme.m */; }; + D032060E2488E38900BB28E7 /* UITableViewHeaderFooterView+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D032060C2488E38900BB28E7 /* UITableViewHeaderFooterView+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D032060F2488E38900BB28E7 /* UITableViewHeaderFooterView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = D032060D2488E38900BB28E7 /* UITableViewHeaderFooterView+QMUI.m */; }; + D033BC0F2549A32D00674526 /* UINavigationItem+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D033BC0D2549A32D00674526 /* UINavigationItem+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D033BC102549A32D00674526 /* UINavigationItem+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = D033BC0E2549A32D00674526 /* UINavigationItem+QMUI.m */; }; + D062F65F22BD0DBD00737AD2 /* UIView+QMUITheme.m in Sources */ = {isa = PBXBuildFile; fileRef = D062F65D22BD0DBD00737AD2 /* UIView+QMUITheme.m */; }; + D09D4BDB24BF1561002D29FF /* UIVisualEffectView+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D09D4BD924BF1561002D29FF /* UIVisualEffectView+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D09D4BDC24BF1561002D29FF /* UIVisualEffectView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = D09D4BDA24BF1561002D29FF /* UIVisualEffectView+QMUI.m */; }; + D0BEFA97247D42510006D1B9 /* UIView+QMUIBadge.h in Headers */ = {isa = PBXBuildFile; fileRef = D0BEFA95247D42510006D1B9 /* UIView+QMUIBadge.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0BEFA98247D42510006D1B9 /* UIView+QMUIBadge.m in Sources */ = {isa = PBXBuildFile; fileRef = D0BEFA96247D42510006D1B9 /* UIView+QMUIBadge.m */; }; + D0BEFA9A247D427A0006D1B9 /* QMUIBadgeProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D0BEFA99247D427A0006D1B9 /* QMUIBadgeProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; D0D0D81A20C2B973000A33D8 /* UIBarItem+QMUIBadge.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D0D81820C2B973000A33D8 /* UIBarItem+QMUIBadge.h */; settings = {ATTRIBUTES = (Public, ); }; }; D0D0D81B20C2B973000A33D8 /* UIBarItem+QMUIBadge.m in Sources */ = {isa = PBXBuildFile; fileRef = D0D0D81920C2B973000A33D8 /* UIBarItem+QMUIBadge.m */; }; + D0ECA054261513230067BCC6 /* NSStringTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D0ECA053261513230067BCC6 /* NSStringTests.m */; }; + D0F0C7C2246A926600927A1A /* QMUICommonDefinesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D0F0C7C1246A926600927A1A /* QMUICommonDefinesTests.m */; }; D0FB669821CBF00F00806600 /* UIInterface+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D0FB669621CBF00F00806600 /* UIInterface+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; D0FB669921CBF00F00806600 /* UIInterface+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = D0FB669721CBF00F00806600 /* UIInterface+QMUI.m */; }; FE1FBCA91E8BA61300C6C01A /* UITextField+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = FE1FBCA71E8BA61300C6C01A /* UITextField+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; FE1FBCAA1E8BA61300C6C01A /* UITextField+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = FE1FBCA81E8BA61300C6C01A /* UITextField+QMUI.m */; }; FE1FBCAF1E8BA79000C6C01A /* UITextView+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = FE1FBCAD1E8BA79000C6C01A /* UITextView+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; FE1FBCB11E8BA79000C6C01A /* UITextView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = FE1FBCAE1E8BA79000C6C01A /* UITextView+QMUI.m */; }; - FEB3638F20D9061000D0F321 /* QMUIVisualEffectView.h in Headers */ = {isa = PBXBuildFile; fileRef = FEB3638D20D9061000D0F321 /* QMUIVisualEffectView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - FEB3639020D9061000D0F321 /* QMUIVisualEffectView.m in Sources */ = {isa = PBXBuildFile; fileRef = FEB3638E20D9061000D0F321 /* QMUIVisualEffectView.m */; }; + FE8710FD22E499EC00DF1354 /* UIMenuController+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = FE8710FB22E499EB00DF1354 /* UIMenuController+QMUI.m */; }; + FE8710FE22E499EC00DF1354 /* UIMenuController+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = FE8710FC22E499EC00DF1354 /* UIMenuController+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FECD352322BBC3BB00DC69DE /* QMUIAnimationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = FECD351D22BBC3BB00DC69DE /* QMUIAnimationHelper.m */; }; + FECD352422BBC3BB00DC69DE /* QMUIEasings.h in Headers */ = {isa = PBXBuildFile; fileRef = FECD351E22BBC3BB00DC69DE /* QMUIEasings.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FECD352522BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.h in Headers */ = {isa = PBXBuildFile; fileRef = FECD351F22BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FECD352622BBC3BB00DC69DE /* QMUIAnimationHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = FECD352022BBC3BB00DC69DE /* QMUIAnimationHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FECD352722BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.m in Sources */ = {isa = PBXBuildFile; fileRef = FECD352122BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.m */; }; + FECD352B22BBC93500DC69DE /* QMUIWindowSizeMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = FECD352922BBC93400DC69DE /* QMUIWindowSizeMonitor.m */; }; + FECD352C22BBC93500DC69DE /* QMUIWindowSizeMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = FECD352A22BBC93500DC69DE /* QMUIWindowSizeMonitor.h */; settings = {ATTRIBUTES = (Public, ); }; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + CD4EA577228C401E00A55066 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CD44C1BD1956D5970098D0A2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = FE0AFAD01D82B9D8000D21D9; + remoteInfo = QMUIKit; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ + 08230CEA233D285B00BF9CB1 /* UISearchController+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UISearchController+QMUI.h"; sourceTree = ""; }; + 08230CEB233D285B00BF9CB1 /* UISearchController+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UISearchController+QMUI.m"; sourceTree = ""; }; + 083551A72438C0D000B8FEAB /* CALayer+QMUIViewAnimation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CALayer+QMUIViewAnimation.h"; sourceTree = ""; }; + 083551A82438C0D000B8FEAB /* CALayer+QMUIViewAnimation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "CALayer+QMUIViewAnimation.m"; sourceTree = ""; }; + 089F1E4A2322F6D50063061E /* UIView+QMUITheme.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+QMUITheme.h"; sourceTree = ""; }; + 08B399C722E18A3B000A8A45 /* UITraitCollection+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITraitCollection+QMUI.h"; sourceTree = ""; }; + 08B399C822E18A3B000A8A45 /* UITraitCollection+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UITraitCollection+QMUI.m"; sourceTree = ""; }; 1178D5672198258700AA30E5 /* NSURL+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSURL+QMUI.h"; sourceTree = ""; }; 1178D5682198258700AA30E5 /* NSURL+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSURL+QMUI.m"; sourceTree = ""; }; - 165F43DC1ADB786E0057EF6A /* QMUIViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIViewController.h; sourceTree = ""; }; - 165F43DD1ADB786E0057EF6A /* QMUIViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIViewController.m; sourceTree = ""; }; + 3CB960C32BB40725005626A6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 6D03A56D1B53895D003BDDE4 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; AA8860B82107455C005E4054 /* QMUIWeakObjectContainer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIWeakObjectContainer.h; sourceTree = ""; }; AA8860B92107455C005E4054 /* QMUIWeakObjectContainer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIWeakObjectContainer.m; sourceTree = ""; }; @@ -283,6 +388,9 @@ CD046C472018688F00092035 /* QMUILogger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUILogger.h; sourceTree = ""; }; CD046C482018688F00092035 /* QMUILogger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUILogger.m; sourceTree = ""; }; CD046C4B2018698200092035 /* QMUILog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUILog.h; sourceTree = ""; }; + CD0A1BA8273512D5002A1A54 /* QMUIStringPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIStringPrivate.h; sourceTree = ""; }; + CD0A1BA9273512D5002A1A54 /* QMUIStringPrivate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIStringPrivate.m; sourceTree = ""; }; + CD0BD68A234F6C34005E47CE /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; CD1817E22010CC4000F8CDEC /* NSNumber+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSNumber+QMUI.m"; sourceTree = ""; }; CD1817E32010CC4000F8CDEC /* NSNumber+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSNumber+QMUI.h"; sourceTree = ""; }; CD18BC7121760D5900E2A4E6 /* QMUINavigationBarScrollingAnimator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUINavigationBarScrollingAnimator.h; sourceTree = ""; }; @@ -290,41 +398,61 @@ CD18CDFC20EE167200EED53C /* UITableViewCell+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITableViewCell+QMUI.h"; sourceTree = ""; }; CD18CDFD20EE167200EED53C /* UITableViewCell+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UITableViewCell+QMUI.m"; sourceTree = ""; }; CD19F4D721E4AB3900BD4687 /* QMUILab.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUILab.h; sourceTree = ""; }; + CD2B196F2A715D6200E8ED18 /* QMUIBadgeLabel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIBadgeLabel.h; sourceTree = ""; }; + CD2B19702A715D6200E8ED18 /* QMUIBadgeLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIBadgeLabel.m; sourceTree = ""; }; CD349BAB2160AF75008653D4 /* QMUIScrollAnimator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIScrollAnimator.h; sourceTree = ""; }; CD349BAC2160AF75008653D4 /* QMUIScrollAnimator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIScrollAnimator.m; sourceTree = ""; }; CD349BB52160B83D008653D4 /* QMUINavigationBarScrollingSnapAnimator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUINavigationBarScrollingSnapAnimator.h; sourceTree = ""; }; CD349BB62160B83D008653D4 /* QMUINavigationBarScrollingSnapAnimator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUINavigationBarScrollingSnapAnimator.m; sourceTree = ""; }; + CD4002192C1F6BB0003D2127 /* QMUIPopupMenuItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIPopupMenuItem.h; sourceTree = ""; }; + CD40021A2C1F6BB0003D2127 /* QMUIPopupMenuItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIPopupMenuItem.m; sourceTree = ""; }; + CD40021D2C1F6E1C003D2127 /* QMUIPopupMenuItemViewProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIPopupMenuItemViewProtocol.h; sourceTree = ""; }; + CD40021F2C1F81CE003D2127 /* QMUIPopupMenuItemView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIPopupMenuItemView.h; sourceTree = ""; }; + CD4002202C1F81CE003D2127 /* QMUIPopupMenuItemView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIPopupMenuItemView.m; sourceTree = ""; }; CD43CB15207B98A10090346B /* QMUIButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIButton.h; sourceTree = ""; }; CD43CB16207B98A10090346B /* QMUIButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIButton.m; sourceTree = ""; }; CD43CB19207B98B60090346B /* QMUINavigationButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUINavigationButton.h; sourceTree = ""; }; CD43CB1A207B98B60090346B /* QMUINavigationButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUINavigationButton.m; sourceTree = ""; }; CD43CB1D207B9A510090346B /* QMUIToolbarButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIToolbarButton.h; sourceTree = ""; }; CD43CB1E207B9A510090346B /* QMUIToolbarButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIToolbarButton.m; sourceTree = ""; }; - CD43CB21207B9ACA0090346B /* QMUILinkButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUILinkButton.h; sourceTree = ""; }; - CD43CB22207B9ACA0090346B /* QMUILinkButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUILinkButton.m; sourceTree = ""; }; - CD43CB25207B9B1A0090346B /* QMUIGhostButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIGhostButton.h; sourceTree = ""; }; - CD43CB26207B9B1A0090346B /* QMUIGhostButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIGhostButton.m; sourceTree = ""; }; - CD43CB29207B9B840090346B /* QMUIFillButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIFillButton.h; sourceTree = ""; }; - CD43CB2A207B9B840090346B /* QMUIFillButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIFillButton.m; sourceTree = ""; }; CD44C1C81956D5970098D0A2 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; CD44C1CA1956D5970098D0A2 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; CD44C1CC1956D5970098D0A2 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; - CD44C1D01956D5970098D0A2 /* qmui-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "qmui-Info.plist"; sourceTree = ""; }; - CD44C1D41956D5970098D0A2 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - CD44C1D71956D5970098D0A2 /* QMUIAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIAppDelegate.h; sourceTree = ""; }; - CD44C1D81956D5970098D0A2 /* QMUIAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIAppDelegate.m; sourceTree = ""; }; - CD44C1DA1956D5970098D0A2 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; CD44C1E11956D5970098D0A2 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; CD4DA9C01E8E3B0500836A1A /* QMUIConfigurationTemplate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIConfigurationTemplate.h; sourceTree = ""; }; CD4DA9C11E8E3B0500836A1A /* QMUIConfigurationTemplate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIConfigurationTemplate.m; sourceTree = ""; }; CD4EA4BD2275FA0100A55066 /* NSMethodSignature+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMethodSignature+QMUI.h"; sourceTree = ""; }; CD4EA4BE2275FA0100A55066 /* NSMethodSignature+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMethodSignature+QMUI.m"; sourceTree = ""; }; + CD4EA571228C401E00A55066 /* QMUIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = QMUIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + CD4EA575228C401E00A55066 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CD4EA57D228C443B00A55066 /* UIColorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UIColorTests.m; sourceTree = ""; }; + CD513E25283527AA004A549D /* QMUIBarProtocolPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIBarProtocolPrivate.h; sourceTree = ""; }; + CD513E26283527AA004A549D /* QMUIBarProtocolPrivate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIBarProtocolPrivate.m; sourceTree = ""; }; + CD513E27283527AA004A549D /* QMUIBarProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIBarProtocol.h; sourceTree = ""; }; + CD513E2B283527CE004A549D /* UITabBar+QMUIBarProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITabBar+QMUIBarProtocol.h"; sourceTree = ""; }; + CD513E2C283527CE004A549D /* UITabBar+QMUIBarProtocol.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UITabBar+QMUIBarProtocol.m"; sourceTree = ""; }; + CD513E2F283527DB004A549D /* UINavigationBar+QMUIBarProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UINavigationBar+QMUIBarProtocol.h"; sourceTree = ""; }; + CD513E30283527DB004A549D /* UINavigationBar+QMUIBarProtocol.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UINavigationBar+QMUIBarProtocol.m"; sourceTree = ""; }; + CD5E431F2B85F71F0030CFDA /* NSRegularExpression+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSRegularExpression+QMUI.h"; sourceTree = ""; }; + CD5E43202B85F71F0030CFDA /* NSRegularExpression+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSRegularExpression+QMUI.m"; sourceTree = ""; }; + CD60DB4F2C5BC5D1005109B3 /* QMUICheckbox.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUICheckbox.h; sourceTree = ""; }; + CD60DB502C5BC5D1005109B3 /* QMUICheckbox.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUICheckbox.m; sourceTree = ""; }; CD6631D91FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUITableViewHeaderFooterView.h; sourceTree = ""; }; CD6631DA1FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUITableViewHeaderFooterView.m; sourceTree = ""; }; + CD669A0B25F79DA40036D6B2 /* UICollectionViewCell+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UICollectionViewCell+QMUI.h"; sourceTree = ""; }; + CD669A0C25F79DA40036D6B2 /* UICollectionViewCell+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UICollectionViewCell+QMUI.m"; sourceTree = ""; }; CD6BE14C2058C64E00BE093E /* QMUICellHeightKeyCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUICellHeightKeyCache.h; sourceTree = ""; }; CD6BE14D2058C64E00BE093E /* QMUICellHeightKeyCache.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUICellHeightKeyCache.m; sourceTree = ""; }; CD6BE1542058C73600BE093E /* UITableView+QMUICellHeightKeyCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITableView+QMUICellHeightKeyCache.h"; sourceTree = ""; }; CD6BE1552058C73600BE093E /* UITableView+QMUICellHeightKeyCache.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UITableView+QMUICellHeightKeyCache.m"; sourceTree = ""; }; + CD70C438276340B300D212F5 /* UISlider+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UISlider+QMUI.h"; sourceTree = ""; }; + CD70C439276340B300D212F5 /* UISlider+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UISlider+QMUI.m"; sourceTree = ""; }; + CD72E7BF2B440DF000AC528A /* QMUILayouterItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUILayouterItem.h; sourceTree = ""; }; + CD72E7C02B440DF000AC528A /* QMUILayouterItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUILayouterItem.m; sourceTree = ""; }; + CD72E7C52B44AF2F00AC528A /* QMUILayouterLinearHorizontal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUILayouterLinearHorizontal.h; sourceTree = ""; }; + CD72E7C62B44AF2F00AC528A /* QMUILayouterLinearHorizontal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUILayouterLinearHorizontal.m; sourceTree = ""; }; + CD72E7C92B44AF8800AC528A /* QMUILayouterLinearVertical.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUILayouterLinearVertical.h; sourceTree = ""; }; + CD72E7CA2B44AF8800AC528A /* QMUILayouterLinearVertical.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUILayouterLinearVertical.m; sourceTree = ""; }; CD745E2821CA5B8E006EC132 /* QMUIImagePreviewView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIImagePreviewView.h; sourceTree = ""; }; CD745E2921CA5B8E006EC132 /* QMUIImagePreviewViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIImagePreviewViewController.m; sourceTree = ""; }; CD745E2A21CA5B8E006EC132 /* QMUIImagePreviewViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIImagePreviewViewController.h; sourceTree = ""; }; @@ -333,6 +461,11 @@ CD745E3121CA5BBB006EC132 /* QMUIImagePreviewViewTransitionAnimator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIImagePreviewViewTransitionAnimator.m; sourceTree = ""; }; CD766F78216B52F3005155BD /* UINavigationBar+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UINavigationBar+QMUI.h"; sourceTree = ""; }; CD766F79216B52F3005155BD /* UINavigationBar+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UINavigationBar+QMUI.m"; sourceTree = ""; }; + 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 = ""; }; @@ -347,13 +480,23 @@ CD8AA7B221E8C0F300BA7369 /* QMUIConsoleViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIConsoleViewController.m; sourceTree = ""; }; CD8AA7C021EDE06800BA7369 /* QMUILog+QMUIConsole.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "QMUILog+QMUIConsole.h"; sourceTree = ""; }; CD8AA7C121EDE06800BA7369 /* QMUILog+QMUIConsole.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "QMUILog+QMUIConsole.m"; sourceTree = ""; }; + CD8CB8C022DE10F200B0C9F8 /* UIImage+QMUITheme.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIImage+QMUITheme.h"; sourceTree = ""; }; + CD8CB8C122DE10F200B0C9F8 /* UIImage+QMUITheme.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIImage+QMUITheme.m"; sourceTree = ""; }; + CD96A2B728C74CCA00E87728 /* NSShadow+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSShadow+QMUI.h"; sourceTree = ""; }; + CD96A2B828C74CCA00E87728 /* NSShadow+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSShadow+QMUI.m"; sourceTree = ""; }; CD979995213F934700C00FDC /* QMUIRuntime.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIRuntime.m; sourceTree = ""; }; CD9D6E6C210C8DEC0004E222 /* QMUILogger+QMUIConfigurationTemplate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "QMUILogger+QMUIConfigurationTemplate.h"; sourceTree = ""; }; CD9D6E6D210C8DEC0004E222 /* QMUILogger+QMUIConfigurationTemplate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "QMUILogger+QMUIConfigurationTemplate.m"; sourceTree = ""; }; - CD9D6EBA210F8CA20004E222 /* CAAnimation+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CAAnimation+QMUI.h"; sourceTree = ""; }; - CD9D6EBB210F8CA20004E222 /* CAAnimation+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "CAAnimation+QMUI.m"; sourceTree = ""; }; + CD9F48A822C3985200F5C5C2 /* QMUIThemePrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIThemePrivate.h; sourceTree = ""; }; + CD9F48A922C3985200F5C5C2 /* QMUIThemePrivate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIThemePrivate.m; sourceTree = ""; }; CDA4083C214F7E2500740888 /* NSCharacterSet+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSCharacterSet+QMUI.h"; sourceTree = ""; }; CDA4083D214F7E2500740888 /* NSCharacterSet+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSCharacterSet+QMUI.m"; sourceTree = ""; }; + CDAA653422BBC1240004C6BB /* UIColor+QMUITheme.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIColor+QMUITheme.h"; sourceTree = ""; }; + CDAA653522BBC1240004C6BB /* UIColor+QMUITheme.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIColor+QMUITheme.m"; sourceTree = ""; }; + CDAA653822BBC3340004C6BB /* QMUIThemeManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIThemeManager.h; sourceTree = ""; }; + CDAA653922BBC3340004C6BB /* QMUIThemeManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIThemeManager.m; sourceTree = ""; }; + CDAB2D242357481700C96B31 /* UITextInputTraits+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITextInputTraits+QMUI.h"; sourceTree = ""; }; + CDAB2D252357481700C96B31 /* UITextInputTraits+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UITextInputTraits+QMUI.m"; sourceTree = ""; }; CDB8CA2E1DCC870700769DF0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; CDB8CA2F1DCC870700769DF0 /* QMUIKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIKit.h; sourceTree = ""; }; CDB8CA761DCC870700769DF0 /* CALayer+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CALayer+QMUI.h"; sourceTree = ""; }; @@ -402,9 +545,9 @@ CDB8CABD1DCC870700769DF0 /* UIViewController+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+QMUI.m"; sourceTree = ""; }; CDB8CABE1DCC870700769DF0 /* UIWindow+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIWindow+QMUI.h"; sourceTree = ""; }; CDB8CABF1DCC870700769DF0 /* UIWindow+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIWindow+QMUI.m"; sourceTree = ""; }; + CDC006E422A804D800A81771 /* NSObjectTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSObjectTests.m; sourceTree = ""; }; CDC163C4204D441000E4CC13 /* QMUILogManagerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUILogManagerViewController.h; sourceTree = ""; }; CDC163C5204D441000E4CC13 /* QMUILogManagerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUILogManagerViewController.m; sourceTree = ""; }; - CDC86F3E1F68D5F8000E8829 /* QMUIResources.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = QMUIResources.bundle; sourceTree = ""; }; CDC86F411F68D5F9000E8829 /* QMUIAsset.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIAsset.h; sourceTree = ""; }; CDC86F421F68D5F9000E8829 /* QMUIAsset.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIAsset.m; sourceTree = ""; }; CDC86F431F68D5F9000E8829 /* QMUIAssetsGroup.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIAssetsGroup.h; sourceTree = ""; }; @@ -465,8 +608,6 @@ CDC86F861F68D5F9000E8829 /* QMUISearchController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUISearchController.m; sourceTree = ""; }; CDC86F871F68D5F9000E8829 /* QMUISegmentedControl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUISegmentedControl.h; sourceTree = ""; }; CDC86F881F68D5F9000E8829 /* QMUISegmentedControl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUISegmentedControl.m; sourceTree = ""; }; - CDC86F891F68D5F9000E8829 /* QMUISlider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUISlider.h; sourceTree = ""; }; - CDC86F8A1F68D5F9000E8829 /* QMUISlider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUISlider.m; sourceTree = ""; }; CDC86F8B1F68D5F9000E8829 /* QMUITableView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUITableView.h; sourceTree = ""; }; CDC86F8C1F68D5F9000E8829 /* QMUITableView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUITableView.m; sourceTree = ""; }; CDC86F8D1F68D5F9000E8829 /* QMUITableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUITableViewCell.h; sourceTree = ""; }; @@ -511,30 +652,64 @@ CDC86FBA1F68D5F9000E8829 /* QMUINavigationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUINavigationController.m; sourceTree = ""; }; CDC86FBB1F68D5F9000E8829 /* QMUITabBarViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUITabBarViewController.h; sourceTree = ""; }; CDC86FBC1F68D5F9000E8829 /* QMUITabBarViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUITabBarViewController.m; sourceTree = ""; }; + CDCD27012B8E0B6200D3500A /* QMUISheetPresentationSupports.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUISheetPresentationSupports.m; sourceTree = ""; }; + CDCD27022B8E0B6200D3500A /* QMUISheetPresentationSupports.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUISheetPresentationSupports.h; sourceTree = ""; }; + CDCD27052B8E0C2700D3500A /* QMUISheetPresentationNavigationBar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUISheetPresentationNavigationBar.h; sourceTree = ""; }; + CDCD27062B8E0C2700D3500A /* QMUISheetPresentationNavigationBar.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUISheetPresentationNavigationBar.m; sourceTree = ""; }; CDD071FB2060F82700343AB6 /* QMUICellHeightCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUICellHeightCache.h; sourceTree = ""; }; CDD071FC2060F82700343AB6 /* QMUICellHeightCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUICellHeightCache.m; sourceTree = ""; }; CDD12D3A1FBB320E00114EA9 /* NSArray+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSArray+QMUI.h"; sourceTree = ""; }; CDD12D3B1FBB320E00114EA9 /* NSArray+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSArray+QMUI.m"; sourceTree = ""; }; + CDD7599C22BBE11200BC8F36 /* QMUITheme.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUITheme.h; sourceTree = ""; }; + CDD759A622BBE68600BC8F36 /* CAAnimation+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CAAnimation+QMUI.m"; sourceTree = ""; }; + CDD759A722BBE68600BC8F36 /* CAAnimation+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CAAnimation+QMUI.h"; sourceTree = ""; }; CDD7C0D3212300A000D6FA1E /* QMUIRuntime.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIRuntime.h; sourceTree = ""; }; - CDD7C2B4212C4E0600D6FA1E /* QMUIPopupMenuItemProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIPopupMenuItemProtocol.h; sourceTree = ""; }; - CDD7C2B6212C4E7400D6FA1E /* QMUIPopupMenuBaseItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIPopupMenuBaseItem.h; sourceTree = ""; }; - CDD7C2B7212C4E7400D6FA1E /* QMUIPopupMenuBaseItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIPopupMenuBaseItem.m; sourceTree = ""; }; - CDD7C2BA212C510B00D6FA1E /* QMUIPopupMenuButtonItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIPopupMenuButtonItem.h; sourceTree = ""; }; - CDD7C2BB212C510B00D6FA1E /* QMUIPopupMenuButtonItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIPopupMenuButtonItem.m; sourceTree = ""; }; CDD7C2BE212C528400D6FA1E /* QMUIPopupMenuView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIPopupMenuView.m; sourceTree = ""; }; CDD7C2BF212C528400D6FA1E /* QMUIPopupMenuView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIPopupMenuView.h; sourceTree = ""; }; CDE418F920761A0F002ED021 /* UIBarItem+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIBarItem+QMUI.h"; sourceTree = ""; }; CDE418FA20761A0F002ED021 /* UIBarItem+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIBarItem+QMUI.m"; sourceTree = ""; }; + CDE77511274E93CE0066A767 /* UIToolbar+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIToolbar+QMUI.h"; sourceTree = ""; }; + CDE77512274E93CE0066A767 /* UIToolbar+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIToolbar+QMUI.m"; sourceTree = ""; }; + CDE77515274FB9430066A767 /* UIBlurEffect+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIBlurEffect+QMUI.h"; sourceTree = ""; }; + CDE77516274FB9430066A767 /* UIBlurEffect+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIBlurEffect+QMUI.m"; sourceTree = ""; }; CDEA6D061F4B07E700F627AF /* UIGestureRecognizer+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIGestureRecognizer+QMUI.h"; sourceTree = ""; }; CDEA6D071F4B07E700F627AF /* UIGestureRecognizer+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIGestureRecognizer+QMUI.m"; sourceTree = ""; }; CDF2D69A207F7E3F009E04DD /* NSPointerArray+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSPointerArray+QMUI.h"; sourceTree = ""; }; CDF2D69B207F7E3F009E04DD /* NSPointerArray+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSPointerArray+QMUI.m"; sourceTree = ""; }; + CDFCDD9E2B43FF07005E1219 /* QMUILayouter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUILayouter.h; sourceTree = ""; }; + CDFE9574293FB1DE007AE1AA /* QMUIKit.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = QMUIKit.podspec; sourceTree = SOURCE_ROOT; }; + CDFF5FB52369926300B63B92 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Photos.framework; sourceTree = DEVELOPER_DIR; }; + D00881752677B5870061CABF /* UIButtonTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UIButtonTests.m; sourceTree = ""; }; + D00B651F242A67D7002C27AB /* QMUIAppearance.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIAppearance.h; sourceTree = ""; }; + D00B6520242A67D7002C27AB /* QMUIAppearance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIAppearance.m; sourceTree = ""; }; + D0193BE622E3449D00FB76F5 /* UIVisualEffect+QMUITheme.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIVisualEffect+QMUITheme.h"; sourceTree = ""; }; + D0193BE722E3449D00FB76F5 /* UIVisualEffect+QMUITheme.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIVisualEffect+QMUITheme.m"; sourceTree = ""; }; + D02096B026DD2B170029BA78 /* UIApplication+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIApplication+QMUI.h"; sourceTree = ""; }; + D02096B126DD2B180029BA78 /* UIApplication+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIApplication+QMUI.m"; sourceTree = ""; }; D021DE35205E809500FFA408 /* UICollectionView+QMUICellSizeKeyCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UICollectionView+QMUICellSizeKeyCache.h"; sourceTree = ""; }; D021DE36205E809500FFA408 /* UICollectionView+QMUICellSizeKeyCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UICollectionView+QMUICellSizeKeyCache.m"; sourceTree = ""; }; D021DE39205E80EB00FFA408 /* QMUICellSizeKeyCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUICellSizeKeyCache.h; sourceTree = ""; }; D021DE3A205E80EB00FFA408 /* QMUICellSizeKeyCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUICellSizeKeyCache.m; sourceTree = ""; }; + D02FDB6C22D880F800DB7E13 /* UISwitch+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UISwitch+QMUI.h"; sourceTree = ""; }; + D02FDB6D22D880F800DB7E13 /* UISwitch+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UISwitch+QMUI.m"; sourceTree = ""; }; + D03102B324A8CB410095C232 /* UIView+QMUIBorder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIView+QMUIBorder.h"; sourceTree = ""; }; + D03102B424A8CB410095C232 /* UIView+QMUIBorder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIView+QMUIBorder.m"; sourceTree = ""; }; + D031843922C287EA00B43520 /* UIViewController+QMUITheme.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIViewController+QMUITheme.h"; sourceTree = ""; }; + D031843A22C287EA00B43520 /* UIViewController+QMUITheme.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+QMUITheme.m"; sourceTree = ""; }; + D032060C2488E38900BB28E7 /* UITableViewHeaderFooterView+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITableViewHeaderFooterView+QMUI.h"; sourceTree = ""; }; + D032060D2488E38900BB28E7 /* UITableViewHeaderFooterView+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UITableViewHeaderFooterView+QMUI.m"; sourceTree = ""; }; + D033BC0D2549A32D00674526 /* UINavigationItem+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UINavigationItem+QMUI.h"; sourceTree = ""; }; + D033BC0E2549A32D00674526 /* UINavigationItem+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UINavigationItem+QMUI.m"; sourceTree = ""; }; + D062F65D22BD0DBD00737AD2 /* UIView+QMUITheme.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIView+QMUITheme.m"; sourceTree = ""; }; + D09D4BD924BF1561002D29FF /* UIVisualEffectView+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIVisualEffectView+QMUI.h"; sourceTree = ""; }; + D09D4BDA24BF1561002D29FF /* UIVisualEffectView+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIVisualEffectView+QMUI.m"; sourceTree = ""; }; + D0BEFA95247D42510006D1B9 /* UIView+QMUIBadge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIView+QMUIBadge.h"; sourceTree = ""; }; + D0BEFA96247D42510006D1B9 /* UIView+QMUIBadge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIView+QMUIBadge.m"; sourceTree = ""; }; + D0BEFA99247D427A0006D1B9 /* QMUIBadgeProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIBadgeProtocol.h; sourceTree = ""; }; D0D0D81820C2B973000A33D8 /* UIBarItem+QMUIBadge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIBarItem+QMUIBadge.h"; sourceTree = ""; }; D0D0D81920C2B973000A33D8 /* UIBarItem+QMUIBadge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIBarItem+QMUIBadge.m"; sourceTree = ""; }; + D0ECA053261513230067BCC6 /* NSStringTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSStringTests.m; sourceTree = ""; }; + D0F0C7C1246A926600927A1A /* QMUICommonDefinesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUICommonDefinesTests.m; sourceTree = ""; }; D0FB669621CBF00F00806600 /* UIInterface+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIInterface+QMUI.h"; sourceTree = ""; }; D0FB669721CBF00F00806600 /* UIInterface+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIInterface+QMUI.m"; sourceTree = ""; }; FE0AFAD11D82B9D8000D21D9 /* QMUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = QMUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -542,15 +717,31 @@ FE1FBCA81E8BA61300C6C01A /* UITextField+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UITextField+QMUI.m"; sourceTree = ""; }; FE1FBCAD1E8BA79000C6C01A /* UITextView+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITextView+QMUI.h"; sourceTree = ""; }; FE1FBCAE1E8BA79000C6C01A /* UITextView+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UITextView+QMUI.m"; sourceTree = ""; }; - FEB3638D20D9061000D0F321 /* QMUIVisualEffectView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIVisualEffectView.h; sourceTree = ""; }; - FEB3638E20D9061000D0F321 /* QMUIVisualEffectView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIVisualEffectView.m; sourceTree = ""; }; + FE8710FB22E499EB00DF1354 /* UIMenuController+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIMenuController+QMUI.m"; sourceTree = ""; }; + FE8710FC22E499EC00DF1354 /* UIMenuController+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIMenuController+QMUI.h"; sourceTree = ""; }; + FECD351D22BBC3BB00DC69DE /* QMUIAnimationHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIAnimationHelper.m; sourceTree = ""; }; + FECD351E22BBC3BB00DC69DE /* QMUIEasings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIEasings.h; sourceTree = ""; }; + FECD351F22BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIDisplayLinkAnimation.h; sourceTree = ""; }; + FECD352022BBC3BB00DC69DE /* QMUIAnimationHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIAnimationHelper.h; sourceTree = ""; }; + FECD352122BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIDisplayLinkAnimation.m; sourceTree = ""; }; + FECD352922BBC93400DC69DE /* QMUIWindowSizeMonitor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIWindowSizeMonitor.m; sourceTree = ""; }; + FECD352A22BBC93500DC69DE /* QMUIWindowSizeMonitor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIWindowSizeMonitor.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + CD4EA56E228C401E00A55066 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CD4EA576228C401E00A55066 /* QMUIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; FE0AFACD1D82B9D8000D21D9 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CDFF5FB62369926300B63B92 /* Photos.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -589,12 +780,6 @@ children = ( CD43CB15207B98A10090346B /* QMUIButton.h */, CD43CB16207B98A10090346B /* QMUIButton.m */, - CD43CB29207B9B840090346B /* QMUIFillButton.h */, - CD43CB2A207B9B840090346B /* QMUIFillButton.m */, - CD43CB25207B9B1A0090346B /* QMUIGhostButton.h */, - CD43CB26207B9B1A0090346B /* QMUIGhostButton.m */, - CD43CB21207B9ACA0090346B /* QMUILinkButton.h */, - CD43CB22207B9ACA0090346B /* QMUILinkButton.m */, CD43CB19207B98B60090346B /* QMUINavigationButton.h */, CD43CB1A207B98B60090346B /* QMUINavigationButton.m */, CD43CB1D207B9A510090346B /* QMUIToolbarButton.h */, @@ -606,9 +791,9 @@ CD44C1BC1956D5970098D0A2 = { isa = PBXGroup; children = ( + CD4EA572228C401E00A55066 /* QMUIKitTests */, CD44C1C71956D5970098D0A2 /* Frameworks */, CD44C1C61956D5970098D0A2 /* Products */, - CD44C1CE1956D5970098D0A2 /* qmui */, CD4DA9BF1E8E3B0500836A1A /* QMUIConfigurationTemplate */, CDB8CA2D1DCC870700769DF0 /* QMUIKit */, ); @@ -618,6 +803,7 @@ isa = PBXGroup; children = ( FE0AFAD11D82B9D8000D21D9 /* QMUIKit.framework */, + CD4EA571228C401E00A55066 /* QMUIKitTests.xctest */, ); name = Products; sourceTree = ""; @@ -626,6 +812,7 @@ isa = PBXGroup; children = ( 6D03A56D1B53895D003BDDE4 /* Photos.framework */, + CDFF5FB52369926300B63B92 /* Photos.framework */, CD44C1C81956D5970098D0A2 /* Foundation.framework */, CD44C1CA1956D5970098D0A2 /* CoreGraphics.framework */, CD44C1CC1956D5970098D0A2 /* UIKit.framework */, @@ -634,35 +821,49 @@ name = Frameworks; sourceTree = ""; }; - CD44C1CE1956D5970098D0A2 /* qmui */ = { + CD4DA9BF1E8E3B0500836A1A /* QMUIConfigurationTemplate */ = { isa = PBXGroup; children = ( - CD44C1D71956D5970098D0A2 /* QMUIAppDelegate.h */, - CD44C1D81956D5970098D0A2 /* QMUIAppDelegate.m */, - 165F43DC1ADB786E0057EF6A /* QMUIViewController.h */, - 165F43DD1ADB786E0057EF6A /* QMUIViewController.m */, - CD44C1DA1956D5970098D0A2 /* Images.xcassets */, - CD44C1CF1956D5970098D0A2 /* Supporting Files */, + CD4DA9C01E8E3B0500836A1A /* QMUIConfigurationTemplate.h */, + CD4DA9C11E8E3B0500836A1A /* QMUIConfigurationTemplate.m */, ); - path = qmui; + path = QMUIConfigurationTemplate; sourceTree = ""; }; - CD44C1CF1956D5970098D0A2 /* Supporting Files */ = { + CD4EA572228C401E00A55066 /* QMUIKitTests */ = { isa = PBXGroup; children = ( - CD44C1D41956D5970098D0A2 /* main.m */, - CD44C1D01956D5970098D0A2 /* qmui-Info.plist */, + D0F0C7C0246A91EF00927A1A /* Core */, + CD7A9A0B22C4AA2F0093DAB4 /* Components */, + CD4EA57C228C43FB00A55066 /* UIKitExtensions */, + CD4EA575228C401E00A55066 /* Info.plist */, ); - name = "Supporting Files"; + path = QMUIKitTests; sourceTree = ""; }; - CD4DA9BF1E8E3B0500836A1A /* QMUIConfigurationTemplate */ = { + CD4EA57C228C43FB00A55066 /* UIKitExtensions */ = { isa = PBXGroup; children = ( - CD4DA9C01E8E3B0500836A1A /* QMUIConfigurationTemplate.h */, - CD4DA9C11E8E3B0500836A1A /* QMUIConfigurationTemplate.m */, + CDC006E422A804D800A81771 /* NSObjectTests.m */, + D0ECA053261513230067BCC6 /* NSStringTests.m */, + CD4EA57D228C443B00A55066 /* UIColorTests.m */, + D00881752677B5870061CABF /* UIButtonTests.m */, ); - path = QMUIConfigurationTemplate; + path = UIKitExtensions; + sourceTree = ""; + }; + CD513E24283527AA004A549D /* QMUIBarProtocol */ = { + isa = PBXGroup; + children = ( + CD513E27283527AA004A549D /* QMUIBarProtocol.h */, + CD513E25283527AA004A549D /* QMUIBarProtocolPrivate.h */, + CD513E26283527AA004A549D /* QMUIBarProtocolPrivate.m */, + CD513E2F283527DB004A549D /* UINavigationBar+QMUIBarProtocol.h */, + CD513E30283527DB004A549D /* UINavigationBar+QMUIBarProtocol.m */, + CD513E2B283527CE004A549D /* UITabBar+QMUIBarProtocol.h */, + CD513E2C283527CE004A549D /* UITabBar+QMUIBarProtocol.m */, + ); + path = QMUIBarProtocol; sourceTree = ""; }; CD6BE1472058C61000BE093E /* QMUICellHeightKeyCache */ = { @@ -689,6 +890,14 @@ path = QMUIImagePreviewView; sourceTree = ""; }; + CD7A9A0B22C4AA2F0093DAB4 /* Components */ = { + isa = PBXGroup; + children = ( + CD7A9A0C22C4AA2F0093DAB4 /* QMUIThemeTests.m */, + ); + path = Components; + sourceTree = ""; + }; CD82C0A42069EB850046EED2 /* QMUIMultipleDelegates */ = { isa = PBXGroup; children = ( @@ -715,13 +924,39 @@ path = QMUIConsole; sourceTree = ""; }; + CDAA653322BBC1070004C6BB /* QMUITheme */ = { + isa = PBXGroup; + children = ( + CDD7599C22BBE11200BC8F36 /* QMUITheme.h */, + CDAA653822BBC3340004C6BB /* QMUIThemeManager.h */, + CDAA653922BBC3340004C6BB /* QMUIThemeManager.m */, + CD7D402D231FA2900007DF6C /* QMUIThemeManagerCenter.h */, + CD7D402E231FA2900007DF6C /* QMUIThemeManagerCenter.m */, + CD9F48A822C3985200F5C5C2 /* QMUIThemePrivate.h */, + CD9F48A922C3985200F5C5C2 /* QMUIThemePrivate.m */, + CDAA653422BBC1240004C6BB /* UIColor+QMUITheme.h */, + CDAA653522BBC1240004C6BB /* UIColor+QMUITheme.m */, + CD8CB8C022DE10F200B0C9F8 /* UIImage+QMUITheme.h */, + CD8CB8C122DE10F200B0C9F8 /* UIImage+QMUITheme.m */, + 089F1E4A2322F6D50063061E /* UIView+QMUITheme.h */, + D062F65D22BD0DBD00737AD2 /* UIView+QMUITheme.m */, + D031843922C287EA00B43520 /* UIViewController+QMUITheme.h */, + D031843A22C287EA00B43520 /* UIViewController+QMUITheme.m */, + D0193BE622E3449D00FB76F5 /* UIVisualEffect+QMUITheme.h */, + D0193BE722E3449D00FB76F5 /* UIVisualEffect+QMUITheme.m */, + ); + path = QMUITheme; + sourceTree = ""; + }; CDB8CA2D1DCC870700769DF0 /* QMUIKit */ = { isa = PBXGroup; children = ( CDB8CA2E1DCC870700769DF0 /* Info.plist */, + 3CB960C32BB40725005626A6 /* PrivacyInfo.xcprivacy */, CDC86F3F1F68D5F9000E8829 /* QMUIComponents */, CDC86FAC1F68D5F9000E8829 /* QMUICore */, CDB8CA2F1DCC870700769DF0 /* QMUIKit.h */, + CDFE9574293FB1DE007AE1AA /* QMUIKit.podspec */, CDC86FB41F68D5F9000E8829 /* QMUIMainFrame */, CDC86F3C1F68D5F8000E8829 /* QMUIResources */, CDB8CA751DCC870700769DF0 /* UIKitExtensions */, @@ -740,6 +975,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 */, @@ -750,20 +987,33 @@ CDB8CA7D1DCC870700769DF0 /* NSParagraphStyle+QMUI.m */, CDF2D69A207F7E3F009E04DD /* NSPointerArray+QMUI.h */, CDF2D69B207F7E3F009E04DD /* NSPointerArray+QMUI.m */, + CD5E431F2B85F71F0030CFDA /* NSRegularExpression+QMUI.h */, + CD5E43202B85F71F0030CFDA /* NSRegularExpression+QMUI.m */, + CD96A2B728C74CCA00E87728 /* NSShadow+QMUI.h */, + CD96A2B828C74CCA00E87728 /* NSShadow+QMUI.m */, CDB8CA7E1DCC870700769DF0 /* NSString+QMUI.h */, CDB8CA7F1DCC870700769DF0 /* NSString+QMUI.m */, 1178D5672198258700AA30E5 /* NSURL+QMUI.h */, 1178D5682198258700AA30E5 /* NSURL+QMUI.m */, + CD513E24283527AA004A549D /* QMUIBarProtocol */, + CD0A1BA8273512D5002A1A54 /* QMUIStringPrivate.h */, + CD0A1BA9273512D5002A1A54 /* QMUIStringPrivate.m */, CDB8CA9A1DCC870700769DF0 /* UIActivityIndicatorView+QMUI.h */, CDB8CA9B1DCC870700769DF0 /* UIActivityIndicatorView+QMUI.m */, + D02096B026DD2B170029BA78 /* UIApplication+QMUI.h */, + D02096B126DD2B180029BA78 /* UIApplication+QMUI.m */, CDE418F920761A0F002ED021 /* UIBarItem+QMUI.h */, CDE418FA20761A0F002ED021 /* UIBarItem+QMUI.m */, CDB8CA9C1DCC870700769DF0 /* UIBezierPath+QMUI.h */, CDB8CA9D1DCC870700769DF0 /* UIBezierPath+QMUI.m */, + CDE77515274FB9430066A767 /* UIBlurEffect+QMUI.h */, + CDE77516274FB9430066A767 /* UIBlurEffect+QMUI.m */, CDB8CA9E1DCC870700769DF0 /* UIButton+QMUI.h */, CDB8CA9F1DCC870700769DF0 /* UIButton+QMUI.m */, CDB8CAA01DCC870700769DF0 /* UICollectionView+QMUI.h */, CDB8CAA11DCC870700769DF0 /* UICollectionView+QMUI.m */, + CD669A0B25F79DA40036D6B2 /* UICollectionViewCell+QMUI.h */, + CD669A0C25F79DA40036D6B2 /* UICollectionViewCell+QMUI.m */, CDB8CAA21DCC870700769DF0 /* UIColor+QMUI.h */, CDB8CAA31DCC870700769DF0 /* UIColor+QMUI.m */, CDB8CAA41DCC870700769DF0 /* UIControl+QMUI.h */, @@ -780,14 +1030,24 @@ D0FB669721CBF00F00806600 /* UIInterface+QMUI.m */, CDB8CAAC1DCC870700769DF0 /* UILabel+QMUI.h */, CDB8CAAD1DCC870700769DF0 /* UILabel+QMUI.m */, + FE8710FC22E499EC00DF1354 /* UIMenuController+QMUI.h */, + FE8710FB22E499EB00DF1354 /* UIMenuController+QMUI.m */, CD766F78216B52F3005155BD /* UINavigationBar+QMUI.h */, CD766F79216B52F3005155BD /* UINavigationBar+QMUI.m */, CDB8CAB01DCC870700769DF0 /* UINavigationController+QMUI.h */, CDB8CAB11DCC870700769DF0 /* UINavigationController+QMUI.m */, + D033BC0D2549A32D00674526 /* UINavigationItem+QMUI.h */, + D033BC0E2549A32D00674526 /* UINavigationItem+QMUI.m */, CDB8CAB21DCC870700769DF0 /* UIScrollView+QMUI.h */, CDB8CAB31DCC870700769DF0 /* UIScrollView+QMUI.m */, CDB8CAB41DCC870700769DF0 /* UISearchBar+QMUI.h */, CDB8CAB51DCC870700769DF0 /* UISearchBar+QMUI.m */, + 08230CEA233D285B00BF9CB1 /* UISearchController+QMUI.h */, + 08230CEB233D285B00BF9CB1 /* UISearchController+QMUI.m */, + CD70C438276340B300D212F5 /* UISlider+QMUI.h */, + CD70C439276340B300D212F5 /* UISlider+QMUI.m */, + D02FDB6C22D880F800DB7E13 /* UISwitch+QMUI.h */, + D02FDB6D22D880F800DB7E13 /* UISwitch+QMUI.m */, CD84F31B1E52DBEA00546111 /* UITabBar+QMUI.h */, CD84F31C1E52DBEA00546111 /* UITabBar+QMUI.m */, CDB8CAB61DCC870700769DF0 /* UITabBarItem+QMUI.h */, @@ -796,14 +1056,26 @@ CDB8CAB91DCC870700769DF0 /* UITableView+QMUI.m */, CD18CDFC20EE167200EED53C /* UITableViewCell+QMUI.h */, CD18CDFD20EE167200EED53C /* UITableViewCell+QMUI.m */, + D032060C2488E38900BB28E7 /* UITableViewHeaderFooterView+QMUI.h */, + D032060D2488E38900BB28E7 /* UITableViewHeaderFooterView+QMUI.m */, FE1FBCA71E8BA61300C6C01A /* UITextField+QMUI.h */, FE1FBCA81E8BA61300C6C01A /* UITextField+QMUI.m */, + CDAB2D242357481700C96B31 /* UITextInputTraits+QMUI.h */, + CDAB2D252357481700C96B31 /* UITextInputTraits+QMUI.m */, FE1FBCAD1E8BA79000C6C01A /* UITextView+QMUI.h */, FE1FBCAE1E8BA79000C6C01A /* UITextView+QMUI.m */, + CDE77511274E93CE0066A767 /* UIToolbar+QMUI.h */, + CDE77512274E93CE0066A767 /* UIToolbar+QMUI.m */, + 08B399C722E18A3B000A8A45 /* UITraitCollection+QMUI.h */, + 08B399C822E18A3B000A8A45 /* UITraitCollection+QMUI.m */, CDB8CABA1DCC870700769DF0 /* UIView+QMUI.h */, CDB8CABB1DCC870700769DF0 /* UIView+QMUI.m */, + D03102B324A8CB410095C232 /* UIView+QMUIBorder.h */, + D03102B424A8CB410095C232 /* UIView+QMUIBorder.m */, CDB8CABC1DCC870700769DF0 /* UIViewController+QMUI.h */, CDB8CABD1DCC870700769DF0 /* UIViewController+QMUI.m */, + D09D4BD924BF1561002D29FF /* UIVisualEffectView+QMUI.h */, + D09D4BDA24BF1561002D29FF /* UIVisualEffectView+QMUI.m */, CDB8CABE1DCC870700769DF0 /* UIWindow+QMUI.h */, CDB8CABF1DCC870700769DF0 /* UIWindow+QMUI.m */, ); @@ -813,7 +1085,7 @@ CDC86F3C1F68D5F8000E8829 /* QMUIResources */ = { isa = PBXGroup; children = ( - CDC86F3E1F68D5F8000E8829 /* QMUIResources.bundle */, + CD0BD68A234F6C34005E47CE /* Images.xcassets */, ); path = QMUIResources; sourceTree = ""; @@ -822,18 +1094,25 @@ isa = PBXGroup; children = ( CDC86F401F68D5F9000E8829 /* AssetLibrary */, - CD9D6EBA210F8CA20004E222 /* CAAnimation+QMUI.h */, - CD9D6EBB210F8CA20004E222 /* CAAnimation+QMUI.m */, + CDD759A722BBE68600BC8F36 /* CAAnimation+QMUI.h */, + CDD759A622BBE68600BC8F36 /* CAAnimation+QMUI.m */, + 083551A72438C0D000B8FEAB /* CALayer+QMUIViewAnimation.h */, + 083551A82438C0D000B8FEAB /* CALayer+QMUIViewAnimation.m */, CDC86F471F68D5F9000E8829 /* ImagePickerLibrary */, CDC86F521F68D5F9000E8829 /* NavigationBarTransition */, CDC86F571F68D5F9000E8829 /* QMUIAlertController.h */, CDC86F581F68D5F9000E8829 /* QMUIAlertController.m */, + FECD351C22BBC3BB00DC69DE /* QMUIAnimation */, + D00B651F242A67D7002C27AB /* QMUIAppearance.h */, + D00B6520242A67D7002C27AB /* QMUIAppearance.m */, D0D0D81720C2B95A000A33D8 /* QMUIBadge */, CD43CB14207B98A10090346B /* QMUIButton */, CDD071FB2060F82700343AB6 /* QMUICellHeightCache.h */, CDD071FC2060F82700343AB6 /* QMUICellHeightCache.m */, CD6BE1472058C61000BE093E /* QMUICellHeightKeyCache */, D021DE34205E801100FFA408 /* QMUICellSizeKeyCache */, + CD60DB4F2C5BC5D1005109B3 /* QMUICheckbox.h */, + CD60DB502C5BC5D1005109B3 /* QMUICheckbox.m */, CDC86F5D1F68D5F9000E8829 /* QMUICollectionViewPagingLayout.h */, CDC86F5E1F68D5F9000E8829 /* QMUICollectionViewPagingLayout.m */, CD8AA7A821E8B9D600BA7369 /* QMUIConsole */, @@ -854,6 +1133,7 @@ CDC86F6E1F68D5F9000E8829 /* QMUIKeyboardManager.m */, CDC86F6F1F68D5F9000E8829 /* QMUILabel.h */, CDC86F701F68D5F9000E8829 /* QMUILabel.m */, + CDFCDD9D2B43FE41005E1219 /* QMUILayouter */, CD046C3E2018665F00092035 /* QMUILog */, CD9D6E6C210C8DEC0004E222 /* QMUILogger+QMUIConfigurationTemplate.h */, CD9D6E6D210C8DEC0004E222 /* QMUILogger+QMUIConfigurationTemplate.m */, @@ -882,8 +1162,7 @@ CDC86F861F68D5F9000E8829 /* QMUISearchController.m */, CDC86F871F68D5F9000E8829 /* QMUISegmentedControl.h */, CDC86F881F68D5F9000E8829 /* QMUISegmentedControl.m */, - CDC86F891F68D5F9000E8829 /* QMUISlider.h */, - CDC86F8A1F68D5F9000E8829 /* QMUISlider.m */, + CDCD27002B8E0B6200D3500A /* QMUISheetPresentation */, CDC86F8B1F68D5F9000E8829 /* QMUITableView.h */, CDC86F8C1F68D5F9000E8829 /* QMUITableView.m */, CDC86F8D1F68D5F9000E8829 /* QMUITableViewCell.h */, @@ -897,12 +1176,13 @@ CDC86F931F68D5F9000E8829 /* QMUITextField.m */, CDC86F941F68D5F9000E8829 /* QMUITextView.h */, CDC86F951F68D5F9000E8829 /* QMUITextView.m */, + CDAA653322BBC1070004C6BB /* QMUITheme */, CDC86F961F68D5F9000E8829 /* QMUITips.h */, CDC86F971F68D5F9000E8829 /* QMUITips.m */, - FEB3638D20D9061000D0F321 /* QMUIVisualEffectView.h */, - FEB3638E20D9061000D0F321 /* QMUIVisualEffectView.m */, AA8860B82107455C005E4054 /* QMUIWeakObjectContainer.h */, AA8860B92107455C005E4054 /* QMUIWeakObjectContainer.m */, + FECD352A22BBC93500DC69DE /* QMUIWindowSizeMonitor.h */, + FECD352922BBC93400DC69DE /* QMUIWindowSizeMonitor.m */, CDC86F9A1F68D5F9000E8829 /* QMUIZoomImageView.h */, CDC86F9B1F68D5F9000E8829 /* QMUIZoomImageView.m */, CDC86F9C1F68D5F9000E8829 /* StaticTableView */, @@ -1012,20 +1292,45 @@ path = QMUIMainFrame; sourceTree = ""; }; + CDCD27002B8E0B6200D3500A /* QMUISheetPresentation */ = { + isa = PBXGroup; + children = ( + CDCD27052B8E0C2700D3500A /* QMUISheetPresentationNavigationBar.h */, + CDCD27062B8E0C2700D3500A /* QMUISheetPresentationNavigationBar.m */, + CDCD27022B8E0B6200D3500A /* QMUISheetPresentationSupports.h */, + CDCD27012B8E0B6200D3500A /* QMUISheetPresentationSupports.m */, + ); + path = QMUISheetPresentation; + sourceTree = ""; + }; CDD7C2B3212C4DED00D6FA1E /* QMUIPopupMenuView */ = { isa = PBXGroup; children = ( - CDD7C2B6212C4E7400D6FA1E /* QMUIPopupMenuBaseItem.h */, - CDD7C2B7212C4E7400D6FA1E /* QMUIPopupMenuBaseItem.m */, - CDD7C2BA212C510B00D6FA1E /* QMUIPopupMenuButtonItem.h */, - CDD7C2BB212C510B00D6FA1E /* QMUIPopupMenuButtonItem.m */, - CDD7C2B4212C4E0600D6FA1E /* QMUIPopupMenuItemProtocol.h */, CDD7C2BF212C528400D6FA1E /* QMUIPopupMenuView.h */, CDD7C2BE212C528400D6FA1E /* QMUIPopupMenuView.m */, + CD4002192C1F6BB0003D2127 /* QMUIPopupMenuItem.h */, + CD40021A2C1F6BB0003D2127 /* QMUIPopupMenuItem.m */, + CD40021D2C1F6E1C003D2127 /* QMUIPopupMenuItemViewProtocol.h */, + CD40021F2C1F81CE003D2127 /* QMUIPopupMenuItemView.h */, + CD4002202C1F81CE003D2127 /* QMUIPopupMenuItemView.m */, ); path = QMUIPopupMenuView; sourceTree = ""; }; + CDFCDD9D2B43FE41005E1219 /* QMUILayouter */ = { + isa = PBXGroup; + children = ( + CDFCDD9E2B43FF07005E1219 /* QMUILayouter.h */, + CD72E7BF2B440DF000AC528A /* QMUILayouterItem.h */, + CD72E7C02B440DF000AC528A /* QMUILayouterItem.m */, + CD72E7C52B44AF2F00AC528A /* QMUILayouterLinearHorizontal.h */, + CD72E7C62B44AF2F00AC528A /* QMUILayouterLinearHorizontal.m */, + CD72E7C92B44AF8800AC528A /* QMUILayouterLinearVertical.h */, + CD72E7CA2B44AF8800AC528A /* QMUILayouterLinearVertical.m */, + ); + path = QMUILayouter; + sourceTree = ""; + }; D021DE34205E801100FFA408 /* QMUICellSizeKeyCache */ = { isa = PBXGroup; children = ( @@ -1040,12 +1345,37 @@ D0D0D81720C2B95A000A33D8 /* QMUIBadge */ = { isa = PBXGroup; children = ( + CD2B196F2A715D6200E8ED18 /* QMUIBadgeLabel.h */, + CD2B19702A715D6200E8ED18 /* QMUIBadgeLabel.m */, + D0BEFA99247D427A0006D1B9 /* QMUIBadgeProtocol.h */, D0D0D81820C2B973000A33D8 /* UIBarItem+QMUIBadge.h */, D0D0D81920C2B973000A33D8 /* UIBarItem+QMUIBadge.m */, + D0BEFA95247D42510006D1B9 /* UIView+QMUIBadge.h */, + D0BEFA96247D42510006D1B9 /* UIView+QMUIBadge.m */, ); path = QMUIBadge; sourceTree = ""; }; + D0F0C7C0246A91EF00927A1A /* Core */ = { + isa = PBXGroup; + children = ( + D0F0C7C1246A926600927A1A /* QMUICommonDefinesTests.m */, + ); + path = Core; + sourceTree = ""; + }; + FECD351C22BBC3BB00DC69DE /* QMUIAnimation */ = { + isa = PBXGroup; + children = ( + FECD352022BBC3BB00DC69DE /* QMUIAnimationHelper.h */, + FECD351D22BBC3BB00DC69DE /* QMUIAnimationHelper.m */, + FECD351F22BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.h */, + FECD352122BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.m */, + FECD351E22BBC3BB00DC69DE /* QMUIEasings.h */, + ); + path = QMUIAnimation; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -1053,12 +1383,48 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + CD60DB512C5BC5D1005109B3 /* QMUICheckbox.h in Headers */, + CD40021E2C1F6E1C003D2127 /* QMUIPopupMenuItemViewProtocol.h in Headers */, + CD4002222C1F81CE003D2127 /* QMUIPopupMenuItemView.h in Headers */, + CD40021C2C1F6BB0003D2127 /* QMUIPopupMenuItem.h in Headers */, + CD81252A2A69CB5900132EC7 /* NSDictionary+QMUI.h in Headers */, + CD96A2B928C74CCA00E87728 /* NSShadow+QMUI.h in Headers */, + CD513E2D283527CE004A549D /* UITabBar+QMUIBarProtocol.h in Headers */, + CD513E31283527DB004A549D /* UINavigationBar+QMUIBarProtocol.h in Headers */, + CD513E2A283527AA004A549D /* QMUIBarProtocol.h in Headers */, + CD70C43A276340B300D212F5 /* UISlider+QMUI.h in Headers */, + CDE77517274FB9430066A767 /* UIBlurEffect+QMUI.h in Headers */, + CDE77513274E93CE0066A767 /* UIToolbar+QMUI.h in Headers */, + D02096B226DD2B180029BA78 /* UIApplication+QMUI.h in Headers */, + CD669A0D25F79DA40036D6B2 /* UICollectionViewCell+QMUI.h in Headers */, + D033BC0F2549A32D00674526 /* UINavigationItem+QMUI.h in Headers */, + D09D4BDB24BF1561002D29FF /* UIVisualEffectView+QMUI.h in Headers */, + D03102B524A8CB410095C232 /* UIView+QMUIBorder.h in Headers */, + D032060E2488E38900BB28E7 /* UITableViewHeaderFooterView+QMUI.h in Headers */, + D0BEFA97247D42510006D1B9 /* UIView+QMUIBadge.h in Headers */, + D0BEFA9A247D427A0006D1B9 /* QMUIBadgeProtocol.h in Headers */, + 083551A92438C0D000B8FEAB /* CALayer+QMUIViewAnimation.h in Headers */, + D00B6521242A67D7002C27AB /* QMUIAppearance.h in Headers */, + CDAB2D262357481700C96B31 /* UITextInputTraits+QMUI.h in Headers */, + 08230CEC233D285B00BF9CB1 /* UISearchController+QMUI.h in Headers */, + CD0BD676233B9888005E47CE /* UIView+QMUITheme.h in Headers */, + D0193BE822E3449D00FB76F5 /* UIVisualEffect+QMUITheme.h in Headers */, + 08B399C922E18A3B000A8A45 /* UITraitCollection+QMUI.h in Headers */, + CD8CB8C222DE10F200B0C9F8 /* UIImage+QMUITheme.h in Headers */, + D02FDB6E22D880F800DB7E13 /* UISwitch+QMUI.h in Headers */, + D031843B22C287EA00B43520 /* UIViewController+QMUITheme.h in Headers */, + CDD759A922BBE68900BC8F36 /* CAAnimation+QMUI.h in Headers */, + CDD7599D22BBE11200BC8F36 /* QMUITheme.h in Headers */, + CDAA653622BBC1240004C6BB /* UIColor+QMUITheme.h in Headers */, + CDAA653A22BBC3340004C6BB /* QMUIThemeManager.h in Headers */, CD4EA4BF2275FA0100A55066 /* NSMethodSignature+QMUI.h in Headers */, + FECD352C22BBC93500DC69DE /* QMUIWindowSizeMonitor.h in Headers */, CD8AA7B321E8C0F300BA7369 /* QMUIConsoleViewController.h in Headers */, CD8AA7C221EDE06800BA7369 /* QMUILog+QMUIConsole.h in Headers */, CD046C452018670900092035 /* QMUILogNameManager.h in Headers */, CD046C412018668900092035 /* QMUILogItem.h in Headers */, CD046C492018688F00092035 /* QMUILogger.h in Headers */, + CD5E43212B85F7200030CFDA /* NSRegularExpression+QMUI.h in Headers */, CD8AA7AF21E8BF0B00BA7369 /* QMUIConsoleToolbar.h in Headers */, CD8AA7AB21E8B9D600BA7369 /* QMUIConsole.h in Headers */, CD19F4D821E4AB3900BD4687 /* QMUILab.h in Headers */, @@ -1071,21 +1437,14 @@ CD766F7A216B52F3005155BD /* UINavigationBar+QMUI.h in Headers */, CD349BB72160B83D008653D4 /* QMUINavigationBarScrollingSnapAnimator.h in Headers */, CD349BAD2160AF75008653D4 /* QMUIScrollAnimator.h in Headers */, + CD0A1BAA273512D5002A1A54 /* QMUIStringPrivate.h in Headers */, CDA4083E214F7E2500740888 /* NSCharacterSet+QMUI.h in Headers */, CDD7C2C1212C528500D6FA1E /* QMUIPopupMenuView.h in Headers */, - CDD7C2BC212C510B00D6FA1E /* QMUIPopupMenuButtonItem.h in Headers */, - CDD7C2B8212C4E7400D6FA1E /* QMUIPopupMenuBaseItem.h in Headers */, - CDD7C2B5212C4E0600D6FA1E /* QMUIPopupMenuItemProtocol.h in Headers */, CDD7C0D4212300A000D6FA1E /* QMUIRuntime.h in Headers */, - CD9D6EBC210F8CA20004E222 /* CAAnimation+QMUI.h in Headers */, CD9D6E6E210C8DEC0004E222 /* QMUILogger+QMUIConfigurationTemplate.h in Headers */, CD18CDFE20EE167200EED53C /* UITableViewCell+QMUI.h in Headers */, - FEB3638F20D9061000D0F321 /* QMUIVisualEffectView.h in Headers */, D0D0D81A20C2B973000A33D8 /* UIBarItem+QMUIBadge.h in Headers */, CDF2D69C207F7E3F009E04DD /* NSPointerArray+QMUI.h in Headers */, - CD43CB23207B9ACA0090346B /* QMUILinkButton.h in Headers */, - CD43CB2B207B9B840090346B /* QMUIFillButton.h in Headers */, - CD43CB27207B9B1A0090346B /* QMUIGhostButton.h in Headers */, CD43CB1B207B98B60090346B /* QMUINavigationButton.h in Headers */, CD43CB17207B98A10090346B /* QMUIButton.h in Headers */, CD43CB1F207B9A510090346B /* QMUIToolbarButton.h in Headers */, @@ -1094,6 +1453,7 @@ CD82C0AF206A2C3D0046EED2 /* QMUIMultipleDelegates.h in Headers */, CDD071FD2060F82700343AB6 /* QMUICellHeightCache.h in Headers */, D021DE37205E809500FFA408 /* UICollectionView+QMUICellSizeKeyCache.h in Headers */, + CD513E28283527AA004A549D /* QMUIBarProtocolPrivate.h in Headers */, D021DE3B205E80EB00FFA408 /* QMUICellSizeKeyCache.h in Headers */, CD6BE1562058C73600BE093E /* UITableView+QMUICellHeightKeyCache.h in Headers */, CD6BE14E2058C64E00BE093E /* QMUICellHeightKeyCache.h in Headers */, @@ -1118,9 +1478,15 @@ CDC86FC01F68D617000E8829 /* QMUIAlbumViewController.h in Headers */, CDC86FC11F68D617000E8829 /* QMUIImagePickerCollectionViewCell.h in Headers */, CDC86FC21F68D617000E8829 /* QMUIImagePickerHelper.h in Headers */, + CDCD27072B8E0C2700D3500A /* QMUISheetPresentationNavigationBar.h in Headers */, + CDCD27042B8E0B6200D3500A /* QMUISheetPresentationSupports.h in Headers */, + CD72E7C72B44AF2F00AC528A /* QMUILayouterLinearHorizontal.h in Headers */, + CD72E7CB2B44AF8800AC528A /* QMUILayouterLinearVertical.h in Headers */, + CD72E7C12B440DF000AC528A /* QMUILayouterItem.h in Headers */, + CDFCDDA02B43FF07005E1219 /* QMUILayouter.h in Headers */, + CD2B19712A715D6200E8ED18 /* QMUIBadgeLabel.h in Headers */, CDC86FC31F68D617000E8829 /* QMUIImagePickerPreviewViewController.h in Headers */, CDC86FC41F68D617000E8829 /* QMUIImagePickerViewController.h in Headers */, - CDC86FC51F68D617000E8829 /* UINavigationBar+Transition.h in Headers */, CDC86FC61F68D617000E8829 /* UINavigationController+NavigationBarTransition.h in Headers */, CDC86FC71F68D617000E8829 /* QMUIAlertController.h in Headers */, CDC86FCA1F68D617000E8829 /* QMUICollectionViewPagingLayout.h in Headers */, @@ -1142,7 +1508,6 @@ CDC86FDD1F68D617000E8829 /* QMUISearchBar.h in Headers */, CDC86FDE1F68D617000E8829 /* QMUISearchController.h in Headers */, CDC86FDF1F68D617000E8829 /* QMUISegmentedControl.h in Headers */, - CDC86FE01F68D617000E8829 /* QMUISlider.h in Headers */, CDC86FE11F68D617000E8829 /* QMUITableView.h in Headers */, CDC86FE21F68D617000E8829 /* QMUITableViewCell.h in Headers */, CDC86FE31F68D617000E8829 /* QMUITableViewProtocols.h in Headers */, @@ -1183,15 +1548,40 @@ CDB8CBAD1DCC870800769DF0 /* UIControl+QMUI.h in Headers */, CDB8CBD51DCC870800769DF0 /* UITableView+QMUI.h in Headers */, CDB8CBDD1DCC870800769DF0 /* UIViewController+QMUI.h in Headers */, + FECD352622BBC3BB00DC69DE /* QMUIAnimationHelper.h in Headers */, + FECD352422BBC3BB00DC69DE /* QMUIEasings.h in Headers */, + FECD352522BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.h in Headers */, + FE8710FE22E499EC00DF1354 /* UIMenuController+QMUI.h in Headers */, + CD7D402F231FA2900007DF6C /* QMUIThemeManagerCenter.h in Headers */, CDB8CBB11DCC870800769DF0 /* UIFont+QMUI.h in Headers */, AA8860BA2107455C005E4054 /* QMUIWeakObjectContainer.h in Headers */, + CDC86FC51F68D617000E8829 /* UINavigationBar+Transition.h in Headers */, CDB8CACF1DCC870700769DF0 /* QMUIKit.h in Headers */, + CD9F48AA22C3985200F5C5C2 /* QMUIThemePrivate.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + CD4EA570228C401E00A55066 /* QMUIKitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = CD4EA57B228C401E00A55066 /* Build configuration list for PBXNativeTarget "QMUIKitTests" */; + buildPhases = ( + CD4EA56D228C401E00A55066 /* Sources */, + CD4EA56E228C401E00A55066 /* Frameworks */, + CD4EA56F228C401E00A55066 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + CD4EA578228C401E00A55066 /* PBXTargetDependency */, + ); + name = QMUIKitTests; + productName = QMUIKitTests; + productReference = CD4EA571228C401E00A55066 /* QMUIKitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; FE0AFAD01D82B9D8000D21D9 /* QMUIKit */ = { isa = PBXNativeTarget; buildConfigurationList = FE0AFAE81D82B9D8000D21D9 /* Build configuration list for PBXNativeTarget "QMUIKit" */; @@ -1221,6 +1611,9 @@ LastUpgradeCheck = 0940; ORGANIZATIONNAME = "QMUI Team"; TargetAttributes = { + CD4EA570228C401E00A55066 = { + CreatedOnToolsVersion = 10.2.1; + }; FE0AFAD01D82B9D8000D21D9 = { CreatedOnToolsVersion = 8.0; DevelopmentTeam = XKMGNG3AWZ; @@ -1229,7 +1622,7 @@ }; }; buildConfigurationList = CD44C1C01956D5970098D0A2 /* Build configuration list for PBXProject "qmui" */; - compatibilityVersion = "Xcode 10.0"; + compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -1244,16 +1637,26 @@ projectRoot = ""; targets = ( FE0AFAD01D82B9D8000D21D9 /* QMUIKit */, + CD4EA570228C401E00A55066 /* QMUIKitTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + CD4EA56F228C401E00A55066 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; FE0AFACF1D82B9D8000D21D9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - CDC870401F68D66A000E8829 /* QMUIResources.bundle in Resources */, + 3CB960C42BB40725005626A6 /* PrivacyInfo.xcprivacy in Resources */, + CD0BD68B234F6C34005E47CE /* Images.xcassets in Resources */, + CDFE9575293FB1DE007AE1AA /* QMUIKit.podspec in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1262,6 +1665,7 @@ /* Begin PBXShellScriptBuildPhase section */ CDE38A6F1F70F745001ACF2C /* Create Umbrella Header File */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -1271,12 +1675,25 @@ outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; - shellPath = "/usr/bin/python ./umbrellaHeaderFileCreator.py"; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n"; + shellPath = /bin/bash; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\npy2_path=\"/usr/bin/python\"\n\nif [ -x \"$py2_path\" ]; then \n /usr/bin/python umbrellaHeaderFileCreator.py\nelse\n /usr/bin/python3 umbrellaHeaderFileCreator.py\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + CD4EA56D228C401E00A55066 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0ECA054261513230067BCC6 /* NSStringTests.m in Sources */, + D00881762677B5870061CABF /* UIButtonTests.m in Sources */, + CD4EA57E228C443B00A55066 /* UIColorTests.m in Sources */, + D0F0C7C2246A926600927A1A /* QMUICommonDefinesTests.m in Sources */, + CDC006E522A804D800A81771 /* NSObjectTests.m in Sources */, + CD7A9A0D22C4AA2F0093DAB4 /* QMUIThemeTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; FE0AFACC1D82B9D8000D21D9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1289,33 +1706,33 @@ CD349BAE2160AF75008653D4 /* QMUIScrollAnimator.m in Sources */, CDB8CB601DCC870700769DF0 /* NSParagraphStyle+QMUI.m in Sources */, CDB8CBB81DCC870800769DF0 /* UIImage+QMUI.m in Sources */, + CD81252B2A69CB5900132EC7 /* NSDictionary+QMUI.m in Sources */, + CD72E7C82B44AF2F00AC528A /* QMUILayouterLinearHorizontal.m in Sources */, CDB8CB581DCC870700769DF0 /* NSAttributedString+QMUI.m in Sources */, CDB8CB5C1DCC870700769DF0 /* NSObject+QMUI.m in Sources */, - FEB3639020D9061000D0F321 /* QMUIVisualEffectView.m in Sources */, CDB8CBA41DCC870700769DF0 /* UIButton+QMUI.m in Sources */, - CDD7C2B9212C4E7400D6FA1E /* QMUIPopupMenuBaseItem.m in Sources */, CDB8CBD81DCC870800769DF0 /* UITableView+QMUI.m in Sources */, CD82C0B0206A2C3D0046EED2 /* QMUIMultipleDelegates.m in Sources */, CDD7C2C0212C528500D6FA1E /* QMUIPopupMenuView.m in Sources */, - CD9D6EBD210F8CA20004E222 /* CAAnimation+QMUI.m in Sources */, CD18CDFF20EE167200EED53C /* UITableViewCell+QMUI.m in Sources */, CDB8CBE41DCC870800769DF0 /* UIWindow+QMUI.m in Sources */, CD43CB18207B98A10090346B /* QMUIButton.m in Sources */, - CDD7C2BD212C510B00D6FA1E /* QMUIPopupMenuButtonItem.m in Sources */, + CD0A1BAB273512D5002A1A54 /* QMUIStringPrivate.m in Sources */, CD6BE14F2058C64E00BE093E /* QMUICellHeightKeyCache.m in Sources */, CDA4083F214F7E2500740888 /* NSCharacterSet+QMUI.m in Sources */, CD046C4A2018688F00092035 /* QMUILogger.m in Sources */, + CD513E29283527AA004A549D /* QMUIBarProtocolPrivate.m in Sources */, CDB8CBCC1DCC870800769DF0 /* UIScrollView+QMUI.m in Sources */, CDB8CBA81DCC870700769DF0 /* UICollectionView+QMUI.m in Sources */, - CD43CB2C207B9B840090346B /* QMUIFillButton.m in Sources */, CD979996213F934700C00FDC /* QMUIRuntime.m in Sources */, - CD43CB24207B9ACA0090346B /* QMUILinkButton.m in Sources */, + CD70C43B276340B300D212F5 /* UISlider+QMUI.m in Sources */, CDB8CBD41DCC870800769DF0 /* UITabBarItem+QMUI.m in Sources */, CD82C0B4206A82520046EED2 /* NSObject+QMUIMultipleDelegates.m in Sources */, CDB8CBC01DCC870800769DF0 /* UILabel+QMUI.m in Sources */, CDEA6D0A1F4B07E700F627AF /* UIGestureRecognizer+QMUI.m in Sources */, CDB8CBB41DCC870800769DF0 /* UIFont+QMUI.m in Sources */, CDB8CBD01DCC870800769DF0 /* UISearchBar+QMUI.m in Sources */, + CD4002212C1F81CE003D2127 /* QMUIPopupMenuItemView.m in Sources */, CD84F31F1E52DBEA00546111 /* UITabBar+QMUI.m in Sources */, FE1FBCB11E8BA79000C6C01A /* UITextView+QMUI.m in Sources */, CD745E3321CA5BBB006EC132 /* QMUIImagePreviewViewTransitionAnimator.m in Sources */, @@ -1323,11 +1740,17 @@ CDB8CB641DCC870700769DF0 /* NSString+QMUI.m in Sources */, CDB8CB9C1DCC870700769DF0 /* UIActivityIndicatorView+QMUI.m in Sources */, CD9D6E6F210C8DEC0004E222 /* QMUILogger+QMUIConfigurationTemplate.m in Sources */, + CDAA653B22BBC3340004C6BB /* QMUIThemeManager.m in Sources */, CDB8CBA01DCC870700769DF0 /* UIBezierPath+QMUI.m in Sources */, + CD60DB522C5BC5D1005109B3 /* QMUICheckbox.m in Sources */, + CD96A2BA28C74CCA00E87728 /* NSShadow+QMUI.m in Sources */, + CD72E7CC2B44AF8800AC528A /* QMUILayouterLinearVertical.m in Sources */, CDB8CBB01DCC870800769DF0 /* UIControl+QMUI.m in Sources */, + D032060F2488E38900BB28E7 /* UITableViewHeaderFooterView+QMUI.m in Sources */, D021DE38205E809500FFA408 /* UICollectionView+QMUICellSizeKeyCache.m in Sources */, CDB8CBE01DCC870800769DF0 /* UIViewController+QMUI.m in Sources */, FE1FBCAA1E8BA61300C6C01A /* UITextField+QMUI.m in Sources */, + D02FDB6F22D880F800DB7E13 /* UISwitch+QMUI.m in Sources */, CDD071FE2060F82700343AB6 /* QMUICellHeightCache.m in Sources */, 1178D56A2198258700AA30E5 /* NSURL+QMUI.m in Sources */, D021DE3C205E80EB00FFA408 /* QMUICellSizeKeyCache.m in Sources */, @@ -1337,15 +1760,19 @@ CDC86FFA1F68D63B000E8829 /* QMUIAsset.m in Sources */, CDC86FFB1F68D63B000E8829 /* QMUIAssetsGroup.m in Sources */, CDC86FFC1F68D63B000E8829 /* QMUIAssetsManager.m in Sources */, + CD513E2E283527CE004A549D /* UITabBar+QMUIBarProtocol.m in Sources */, CDC86FFD1F68D63B000E8829 /* QMUIAlbumViewController.m in Sources */, CDC86FFE1F68D63B000E8829 /* QMUIImagePickerCollectionViewCell.m in Sources */, CDC86FFF1F68D63B000E8829 /* QMUIImagePickerHelper.m in Sources */, CDC870001F68D63B000E8829 /* QMUIImagePickerPreviewViewController.m in Sources */, CDC870011F68D63B000E8829 /* QMUIImagePickerViewController.m in Sources */, CDC870021F68D63B000E8829 /* UINavigationBar+Transition.m in Sources */, + D00B6522242A67D7002C27AB /* QMUIAppearance.m in Sources */, CD349BB82160B83D008653D4 /* QMUINavigationBarScrollingSnapAnimator.m in Sources */, + 08B399CA22E18A3B000A8A45 /* UITraitCollection+QMUI.m in Sources */, CD766F7B216B52F3005155BD /* UINavigationBar+QMUI.m in Sources */, CD43CB1C207B98B60090346B /* QMUINavigationButton.m in Sources */, + CDAA653722BBC1240004C6BB /* UIColor+QMUITheme.m in Sources */, CDC870031F68D63B000E8829 /* UINavigationController+NavigationBarTransition.m in Sources */, CDC870041F68D63B000E8829 /* QMUIAlertController.m in Sources */, CDC870071F68D63B000E8829 /* QMUICollectionViewPagingLayout.m in Sources */, @@ -1355,7 +1782,12 @@ CDC870091F68D63B000E8829 /* QMUIEmotionView.m in Sources */, CDC8700A1F68D63B000E8829 /* QMUIEmptyView.m in Sources */, CDC8700B1F68D63B000E8829 /* QMUIFloatLayoutView.m in Sources */, + CD40021B2C1F6BB0003D2127 /* QMUIPopupMenuItem.m in Sources */, + FECD352322BBC3BB00DC69DE /* QMUIAnimationHelper.m in Sources */, CD046C422018668900092035 /* QMUILogItem.m in Sources */, + CD5E43222B85F7200030CFDA /* NSRegularExpression+QMUI.m in Sources */, + D0BEFA98247D42510006D1B9 /* UIView+QMUIBadge.m in Sources */, + D033BC102549A32D00674526 /* UINavigationItem+QMUI.m in Sources */, CDC8700C1F68D63B000E8829 /* QMUIGridView.m in Sources */, CD046C462018670900092035 /* QMUILogNameManager.m in Sources */, CDC8700F1F68D63B000E8829 /* QMUIKeyboardManager.m in Sources */, @@ -1367,32 +1799,51 @@ AA8860BB2107455C005E4054 /* QMUIWeakObjectContainer.m in Sources */, CDC870131F68D63B000E8829 /* QMUIMoreOperationController.m in Sources */, CD4EA4C02275FA0100A55066 /* NSMethodSignature+QMUI.m in Sources */, + D0193BE922E3449D00FB76F5 /* UIVisualEffect+QMUITheme.m in Sources */, CDC870141F68D63B000E8829 /* QMUINavigationTitleView.m in Sources */, CDC870151F68D63B000E8829 /* QMUIOrderedDictionary.m in Sources */, + CDE77514274E93CE0066A767 /* UIToolbar+QMUI.m in Sources */, CD8AA7B421E8C0F300BA7369 /* QMUIConsoleViewController.m in Sources */, CD8AA7C321EDE06800BA7369 /* QMUILog+QMUIConsole.m in Sources */, + CD9F48AB22C3985200F5C5C2 /* QMUIThemePrivate.m in Sources */, CDC870161F68D63B000E8829 /* QMUIPieProgressView.m in Sources */, CDC870171F68D63B000E8829 /* QMUIPopupContainerView.m in Sources */, + CDE77518274FB9430066A767 /* UIBlurEffect+QMUI.m in Sources */, + CDCD27082B8E0C2700D3500A /* QMUISheetPresentationNavigationBar.m in Sources */, CDC163C7204D441000E4CC13 /* QMUILogManagerViewController.m in Sources */, CDC870191F68D63B000E8829 /* QMUIEmotionInputManager.m in Sources */, + CD669A0E25F79DA40036D6B2 /* UICollectionViewCell+QMUI.m in Sources */, CDC8701A1F68D63B000E8829 /* QMUISearchBar.m in Sources */, CDC8701B1F68D63B000E8829 /* QMUISearchController.m in Sources */, + D031843C22C287EA00B43520 /* UIViewController+QMUITheme.m in Sources */, CD1817E42010CC4000F8CDEC /* NSNumber+QMUI.m in Sources */, + CD7D4030231FA2900007DF6C /* QMUIThemeManagerCenter.m in Sources */, CDC8701C1F68D63B000E8829 /* QMUISegmentedControl.m in Sources */, - CDC8701D1F68D63B000E8829 /* QMUISlider.m in Sources */, CD43CB20207B9A510090346B /* QMUIToolbarButton.m in Sources */, CDC8701E1F68D63B000E8829 /* QMUITableView.m in Sources */, CDC8701F1F68D63B000E8829 /* QMUITableViewCell.m in Sources */, CDC870201F68D63B000E8829 /* QMUITestView.m in Sources */, + CDCD27032B8E0B6200D3500A /* QMUISheetPresentationSupports.m in Sources */, CDC870211F68D63B000E8829 /* QMUITextField.m in Sources */, + CDD759A822BBE68900BC8F36 /* CAAnimation+QMUI.m in Sources */, + FECD352B22BBC93500DC69DE /* QMUIWindowSizeMonitor.m in Sources */, CDC870221F68D63B000E8829 /* QMUITextView.m in Sources */, CDC870231F68D63B000E8829 /* QMUITips.m in Sources */, CDC870251F68D63B000E8829 /* QMUIZoomImageView.m in Sources */, + CDAB2D272357481700C96B31 /* UITextInputTraits+QMUI.m in Sources */, + FE8710FD22E499EC00DF1354 /* UIMenuController+QMUI.m in Sources */, + 083551AA2438C0D000B8FEAB /* CALayer+QMUIViewAnimation.m in Sources */, + CD72E7C22B440DF000AC528A /* QMUILayouterItem.m in Sources */, + D09D4BDC24BF1561002D29FF /* UIVisualEffectView+QMUI.m in Sources */, + D062F65F22BD0DBD00737AD2 /* UIView+QMUITheme.m in Sources */, CDC870261F68D63B000E8829 /* QMUIStaticTableViewCellData.m in Sources */, CDC870271F68D63B000E8829 /* QMUIStaticTableViewCellDataSource.m in Sources */, CD745E2D21CA5B8F006EC132 /* QMUIImagePreviewViewController.m in Sources */, + D02096B326DD2B180029BA78 /* UIApplication+QMUI.m in Sources */, CD745E2F21CA5B8F006EC132 /* QMUIImagePreviewView.m in Sources */, + 08230CED233D285B00BF9CB1 /* UISearchController+QMUI.m in Sources */, CDC870281F68D63B000E8829 /* UITableView+QMUIStaticCell.m in Sources */, + CD2B19722A715D6200E8ED18 /* QMUIBadgeLabel.m in Sources */, CDC870291F68D63B000E8829 /* QMUIToastAnimator.m in Sources */, D0D0D81B20C2B973000A33D8 /* UIBarItem+QMUIBadge.m in Sources */, CDC8702A1F68D63B000E8829 /* QMUIToastBackgroundView.m in Sources */, @@ -1401,16 +1852,27 @@ CDC8702C1F68D63B000E8829 /* QMUIToastView.m in Sources */, CDC8702D1F68D63B000E8829 /* QMUIConfiguration.m in Sources */, CDC8702E1F68D63B000E8829 /* QMUIHelper.m in Sources */, + CD8CB8C322DE10F200B0C9F8 /* UIImage+QMUITheme.m in Sources */, CDC8702F1F68D63B000E8829 /* QMUICommonTableViewController.m in Sources */, CDC870301F68D63B000E8829 /* QMUICommonViewController.m in Sources */, CDC870311F68D63B000E8829 /* QMUINavigationController.m in Sources */, + FECD352722BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.m in Sources */, CDC870321F68D63B000E8829 /* QMUITabBarViewController.m in Sources */, - CD43CB28207B9B1A0090346B /* QMUIGhostButton.m in Sources */, + D03102B624A8CB410095C232 /* UIView+QMUIBorder.m in Sources */, + CD513E32283527DB004A549D /* UINavigationBar+QMUIBarProtocol.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + CD4EA578228C401E00A55066 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FE0AFAD01D82B9D8000D21D9 /* QMUIKit */; + targetProxy = CD4EA577228C401E00A55066 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ CD44C1EF1956D5970098D0A2 /* Debug */ = { isa = XCBuildConfiguration; @@ -1461,7 +1923,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; }; @@ -1509,12 +1971,73 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; name = Release; }; + CD4EA579228C401E00A55066 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = QMUIKitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.qmui.QMUIKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CD4EA57A228C401E00A55066 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = QMUIKitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.qmui.QMUIKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; FE0AFAE91D82B9D8000D21D9 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1539,13 +2062,14 @@ GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; INFOPLIST_FILE = QMUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACH_O_TYPE = mh_dylib; + MARKETING_VERSION = 4.8.0; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = com.qmui.QMUIKit; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1582,13 +2106,14 @@ GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; INFOPLIST_FILE = QMUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACH_O_TYPE = mh_dylib; + MARKETING_VERSION = 4.8.0; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = com.qmui.QMUIKit; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1612,6 +2137,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + CD4EA57B228C401E00A55066 /* Build configuration list for PBXNativeTarget "QMUIKitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CD4EA579228C401E00A55066 /* Debug */, + CD4EA57A228C401E00A55066 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; FE0AFAE81D82B9D8000D21D9 /* Build configuration list for PBXNativeTarget "QMUIKit" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/QMUI/qmui.xcodeproj/xcshareddata/xcschemes/QMUIKit.xcscheme b/QMUI/qmui.xcodeproj/xcshareddata/xcschemes/QMUIKit.xcscheme index 38617255..ab8ba0ba 100644 --- a/QMUI/qmui.xcodeproj/xcshareddata/xcschemes/QMUIKit.xcscheme +++ b/QMUI/qmui.xcodeproj/xcshareddata/xcschemes/QMUIKit.xcscheme @@ -1,6 +1,6 @@ - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/QMUI/qmui.xcodeproj/xcuserdata/molice.xcuserdatad/xcschemes/xcschememanagement.plist b/QMUI/qmui.xcodeproj/xcuserdata/molice.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index ccc13613..00000000 --- a/QMUI/qmui.xcodeproj/xcuserdata/molice.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,27 +0,0 @@ - - - - - SchemeUserState - - QMUIKit.xcscheme_^#shared#^_ - - orderHint - 0 - - - SuppressBuildableAutocreation - - 16E46D491B00D8C1002B7DB8 - - primary - - - FE0AFAD01D82B9D8000D21D9 - - primary - - - - - diff --git a/QMUI/qmui/Images.xcassets/Contents.json b/QMUI/qmui/Images.xcassets/Contents.json deleted file mode 100644 index da4a164c..00000000 --- a/QMUI/qmui/Images.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/QMUI/qmui/QMUIAppDelegate.m b/QMUI/qmui/QMUIAppDelegate.m deleted file mode 100644 index 7ae5f02c..00000000 --- a/QMUI/qmui/QMUIAppDelegate.m +++ /dev/null @@ -1,51 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUIAppDelegate.m -// qmui -// -// Created by QMUI Team on 14-6-22. -// - -#import "QMUIAppDelegate.h" - -@implementation QMUIAppDelegate - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - return YES; -} - -- (void)applicationWillResignActive:(UIApplication *)application -{ - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. -} - -- (void)applicationDidEnterBackground:(UIApplication *)application -{ - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. -} - -- (void)applicationWillEnterForeground:(UIApplication *)application -{ - // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. -} - -- (void)applicationDidBecomeActive:(UIApplication *)application -{ - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. -} - -- (void)applicationWillTerminate:(UIApplication *)application -{ - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. -} - -@end diff --git a/QMUI/qmui/QMUIViewController.m b/QMUI/qmui/QMUIViewController.m deleted file mode 100644 index 5df392e8..00000000 --- a/QMUI/qmui/QMUIViewController.m +++ /dev/null @@ -1,57 +0,0 @@ -/***** - * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 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. - *****/ - -// -// QMUIViewController.m -// qmui -// -// Created by QMUI Team on 15/4/13. -// - -#import "QMUIViewController.h" - -@interface QMUIViewController () - -@end - -@implementation QMUIViewController -{ - UILabel *_homeLabel; -} - -- (void)viewDidLoad { - [super viewDidLoad]; -} - -- (void)didReceiveMemoryWarning { - [super didReceiveMemoryWarning]; -} - -- (void)initSubviews { - [super initSubviews]; - _homeLabel = [[UILabel alloc] init]; - _homeLabel.numberOfLines = 0; - _homeLabel.textColor = UIColorGray; - _homeLabel.text = @"欢迎使用 QMUI,如需了解 QMUI 的使用,请下载 QMUI Demo 项目进行查看。"; - [self.view addSubview:_homeLabel]; -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - CGFloat paddingHorizontal = 20; - CGFloat homeLabelLimitWidth = CGRectGetWidth(self.view.bounds) - paddingHorizontal * 2; - CGSize homeLabelSize = [_homeLabel sizeThatFits:CGSizeMake(homeLabelLimitWidth, CGFLOAT_MAX)]; - _homeLabel.frame = CGRectMake(paddingHorizontal, self.qmui_navigationBarMaxYInViewCoordinator + 50, homeLabelLimitWidth, homeLabelSize.height); -} - -- (void)setupNavigationItems { - [super setupNavigationItems]; - self.title = @"QMUI"; -} - -@end diff --git a/QMUI/qmui/qmui-Info.plist b/QMUI/qmui/qmui-Info.plist deleted file mode 100644 index 86b0446c..00000000 --- a/QMUI/qmui/qmui-Info.plist +++ /dev/null @@ -1,41 +0,0 @@ - - - - - CFBundleDevelopmentRegion - zh_CN - CFBundleDisplayName - ${PRODUCT_NAME} - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ${PRODUCT_NAME} - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - LSRequiresIPhoneOS - - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - UIInterfaceOrientationPortraitUpsideDown - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/QMUI/umbrellaHeaderFileCreator.py b/QMUI/umbrellaHeaderFileCreator.py index 63e84d57..66df3f7a 100644 --- a/QMUI/umbrellaHeaderFileCreator.py +++ b/QMUI/umbrellaHeaderFileCreator.py @@ -11,26 +11,30 @@ infoFilePath = str(os.getenv('SRCROOT')) + '/QMUIKit/Info.plist' infoTree = ET.parse(infoFilePath) infoDictList = list(infoTree.find('dict')) -versionString = '0.0.0' +versionString = '' for index in range(len(infoDictList)): element = infoDictList[index] if element.text == 'CFBundleShortVersionString': versionString = infoDictList[index + 1].text break +if versionString.startswith('$'): + versionEnvName = versionString[2:-1] + versionString = os.getenv(versionEnvName) + print('umbrella creator: bundle versions string is %s, env name is %s' % (versionString, versionEnvName)) # 读取头文件准备生成 umbrella file publicHeaderFilePath = str(os.getenv('BUILT_PRODUCTS_DIR')) + '/' + os.getenv('PUBLIC_HEADERS_FOLDER_PATH') -print 'umbrella creator: publicHeaderFilePath = ' + publicHeaderFilePath +print('umbrella creator: publicHeaderFilePath = ' + publicHeaderFilePath) umbrellaHeaderFileName = 'QMUIKit.h' umbrellaHeaderFilePath = str(os.getenv('SRCROOT')) + '/QMUIKit/' + umbrellaHeaderFileName -print 'umbrella creator: umbrellaHeaderFilePath = ' + umbrellaHeaderFilePath -umbrellaFileContent = '''/***** +print('umbrella creator: umbrellaHeaderFilePath = ' + umbrellaHeaderFilePath) +umbrellaFileContent = '''/** * Tencent is pleased to support the open source community by making QMUI_iOS available. - * Copyright (C) 2016-2019 THL A29 Limited, a Tencent company. All rights reserved. + * 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. - *****/ + */ /// Automatically created by script in Build Phases @@ -61,10 +65,10 @@ f.seek(0) oldFileContent = f.read().strip() if oldFileContent == umbrellaFileContent: - print 'umbrella creator: ' + umbrellaHeaderFileName + '的内容没有变化,不需要重写' + print('umbrella creator: ' + umbrellaHeaderFileName + '的内容没有变化,不需要重写') else: - print 'umbrella creator: ' + umbrellaHeaderFileName + '的内容发生变化,开始重写' - print 'umbrella creator: umbrellaFileContent = ' + umbrellaFileContent + print('umbrella creator: ' + umbrellaHeaderFileName + '的内容发生变化,开始重写') + print('umbrella creator: umbrellaFileContent = ' + umbrellaFileContent) f.seek(0) f.write(umbrellaFileContent) diff --git a/QMUIDemoUITests/Info.plist b/QMUIDemoUITests/Info.plist new file mode 100644 index 00000000..6c40a6cd --- /dev/null +++ b/QMUIDemoUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/QMUIDemoUITests/QDUITestTools.h b/QMUIDemoUITests/QDUITestTools.h new file mode 100644 index 00000000..2fa547c7 --- /dev/null +++ b/QMUIDemoUITests/QDUITestTools.h @@ -0,0 +1,70 @@ +// +// QDUITestTools.h +// QMUIDemoUITests +// +// Created by MoLice on 2019/M/17. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +CG_INLINE BOOL +CGFloatAboutEqualWithEpsilon(CGFloat f1, CGFloat f2, CGFloat epsilon) { + return ABS(f1 - f2) <= epsilon; +} + +CG_INLINE BOOL +CGFloatAboutEqualToFloat(CGFloat f1, CGFloat f2) { + return CGFloatAboutEqualWithEpsilon(f1, f2, PixelOne); +} + +CG_INLINE BOOL +CGPointAboutEqualToPoint(CGPoint p1, CGPoint p2) { + return CGFloatAboutEqualToFloat(p1.x, p2.x) && CGFloatAboutEqualToFloat(p1.y, p2.y); +} + +CG_INLINE BOOL +CGSizeAboutEqualToSize(CGSize s1, CGSize s2) { + return CGFloatAboutEqualToFloat(s1.width, s2.width) && CGFloatAboutEqualToFloat(s1.height, s2.height); +} + +CG_INLINE BOOL +CGRectAboutEqualToRect(CGRect r1, CGRect r2) { + return CGPointAboutEqualToPoint(r1.origin, r2.origin) && CGSizeAboutEqualToSize(r1.size, r2.size); +} + +@interface QDUITestTools : NSObject + +@end + +@interface XCUIElement (QDUITest) + +/// 判断当前元素是否正处于键盘聚焦编辑状态 +@property(nonatomic, assign, readonly) BOOL qd_hasKeyboardFocus; + +/// 清空输入框已输入的文字 +- (void)qd_clearText; +@end + +typedef NS_ENUM(NSUInteger, QDUITestMenuItem) { + QDUITestMenuItemSelect, + QDUITestMenuItemSelectAll, + QDUITestMenuItemPaste, + QDUITestMenuItemCut, + QDUITestMenuItemCopy, + QDUITestMenuItemReplace, + QDUITestMenuItemLookUp, + QDUITestMenuItemShare +}; + +@interface XCUIApplication (QDUITest) + +- (void)qd_tapMenuItem:(QDUITestMenuItem)menuItem; +- (XCUIElement *)qd_rootViewOfControllerTitle:(NSString *)title; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUIDemoUITests/QDUITestTools.m b/QMUIDemoUITests/QDUITestTools.m new file mode 100644 index 00000000..f3950662 --- /dev/null +++ b/QMUIDemoUITests/QDUITestTools.m @@ -0,0 +1,64 @@ +// +// QDUITestTools.m +// QMUIDemoUITests +// +// Created by MoLice on 2019/M/17. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import "QDUITestTools.h" + +@implementation QDUITestTools + +@end + +@implementation XCUIElement (QDUITest) + +- (BOOL)qd_hasKeyboardFocus { + // https://stackoverflow.com/a/35915719/4250833 + if ([self isKindOfClass:[UIView class]] && QMUICMIActivated && !IgnoreKVCAccessProhibited) { + BeginIgnoreUIKVCAccessProhibited + id value = [self valueForKey:@"hasKeyboardFocus"]; + EndIgnoreUIKVCAccessProhibited + return value; + } + return [self valueForKey:@"hasKeyboardFocus"]; +} + +- (void)qd_clearText { + if ([self.value isKindOfClass:[NSString class]]) { + while (((NSString *)self.value).length > 0 && ![self.value isEqualToString:self.placeholderValue]) { + [self typeText:XCUIKeyboardKeyDelete]; + } + } +} + +@end + +@implementation XCUIApplication (QDUITest) + +- (void)qd_tapMenuItem:(QDUITestMenuItem)menuItem { + if (!self.menuItems.count) return; + NSArray *> *items = @[@[@"选择", @"Select"], + @[@"全选", @"Select All"], + @[@"粘贴", @"Paste"], + @[@"剪切", @"Cut"], + @[@"拷贝", @"Copy"], + @[@"替换", @"Replace…"], + @[@"查找", @"Look Up"], + @[@"分享", @"Share…"], + ]; + NSArray *titles = items[menuItem]; + for (NSString *title in titles) { + if (self.menuItems[title].exists) { + [self.menuItems[title] tap]; + break; + } + } +} + +- (XCUIElement *)qd_rootViewOfControllerTitle:(NSString *)title { + return self.otherElements[[NSString stringWithFormat:@"viewController-%@", title]]; +} + +@end diff --git a/QMUIDemoUITests/QMUIDemoUITests.m b/QMUIDemoUITests/QMUIDemoUITests.m new file mode 100644 index 00000000..fea13cd6 --- /dev/null +++ b/QMUIDemoUITests/QMUIDemoUITests.m @@ -0,0 +1,138 @@ +// +// QMUIDemoUITests.m +// QMUIDemoUITests +// +// Created by MoLice on 2019/M/16. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import +#import "QDUITestTools.h" +#import + +@interface QMUIDemoUITests : XCTestCase + +@property(nonatomic, strong) XCUIApplication *app; +@end + +@implementation QMUIDemoUITests + +- (void)setUp { + + self.app = [[XCUIApplication alloc] init]; + self.app.launchEnvironment = @{@"isUITest": @YES};// 给业务代码通过 NSProcessInfo.processInfo.environment[@"isUITest"] 来判断当前是否正在运行 UITest + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + self.continueAfterFailure = YES; + + // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. + [self.app launch]; + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. +} + +- (void)testUIKit { + [self _testQMUITextView]; + [self _testQMUINavigationController]; +} + +- (void)_testQMUITextView { + [self tapGridButton:@"QMUITextView"]; + + XCUIElement *textView = self.app.textViews.firstMatch; + NSUInteger keyboardCount = self.app.keyboards.count; + NSString *placeholder = textView.placeholderValue; + + [textView tap]; + keyboardCount++; + XCTAssertTrue(self.app.keyboards.count == keyboardCount); + + // 输入文字则 placeholder 消失,清空文字则 placeholder 出现 + XCTAssertTrue(textView.staticTexts[placeholder].exists); + [textView typeText:@"text"]; + XCTAssertFalse(textView.staticTexts[placeholder].exists); + [textView qd_clearText]; + XCTAssertTrue(textView.staticTexts[placeholder].exists); + + // 选择、复制、粘贴等 menu 操作 + [textView typeText:@"string to be copied"]; + XCTAssertFalse(self.app.menuItems.count); + [textView pressForDuration:1]; + sleep(1.5); + XCTAssertTrue([[self.app menuItems] count]); + [self.app qd_tapMenuItem:QDUITestMenuItemSelectAll]; + sleep(1.5); + [self.app qd_tapMenuItem:QDUITestMenuItemCopy]; + sleep(1.5); + NSLog(@"UIPasteboard.generalPasteboard.string = %@", UIPasteboard.generalPasteboard.string); + XCTAssertEqualObjects(UIPasteboard.generalPasteboard.string, textView.value);// typeText: 在 iOS 10 下会被输入法的自动联想影响,实际输入并不是这个,所以直接取 textView 当前的文字来比较才最稳妥 + + // 文字高度自适应 + [self.app.keys[@"delete"] tap]; + CGFloat height = CGRectGetHeight(textView.frame); + [textView typeText:@"这是很长一段文字,触发高度变化。这是很长一段文字,触发高度变化。这是很长一段文字,触发高度变化。这是很长一段文字,触发高度变化。这是很长一段文字,触发高度变化。这是很长一段文字,触发高度变化。"]; + XCTAssertTrue(CGRectGetHeight(textView.frame) > height); + + // 输入超过一定字符就无法再输入 + [textView typeText:@"0123456789"]; + XCTAssertTrue([((NSString *)textView.value) hasSuffix:@"3"]);// 前面一长串文字个数为96,所以后面输入3后理应就无法再输入了 + + sleep(2);// 等待 QMUITips 消失才能点击 textView + + // 粘贴过来的一大段文字也要自动截断 + [textView tap]; + sleep(1.5); + [self.app qd_tapMenuItem:QDUITestMenuItemSelectAll]; + [self.app.keys[@"delete"] tap]; + NSString *pasteString = @"这是一段粘贴过来的长文本,末尾理应会被截断。这是一段粘贴过来的长文本,末尾理应会被截断。这是一段粘贴过来的长文本,末尾理应会被截断。这是一段粘贴过来的长文本,末尾理应会被截断。这是一段粘贴过来的长文本,末尾理应会被截断。"; + [textView typeText:pasteString]; + XCTAssertTrue(((NSString *)textView.value).length < pasteString.length); + + /* + TODO: molice + 1. 用户手动输入文字、程序 setText:,max length 都要能正常工作 + 2. 即将输入的文字被完全拦截、部分截断,都应该触发 textViewDidChange: 这个 delegate + 3. 输入的文字被拦截一部分后刚好换行了,此时应该触发 textView:newHeightAfterTextChanged: https://github.com/Tencent/QMUI_iOS/issues/1120 + */ + + sleep(1); + + // 点击空白降下键盘 + [[self.app qd_rootViewOfControllerTitle:@"QMUITextView"] tap]; + keyboardCount--; + XCTAssertTrue(self.app.keyboards.count == keyboardCount); +} + +- (void)_testQMUINavigationController { + [self tapGridButton:@"QMUINavigationController"]; + [self.app.tables.cells.staticTexts[@"拦截系统navBar返回按钮事件"] tap]; + [self.app.textViews.firstMatch typeText:@"text"]; + + [self.app.navigationBars.firstMatch.buttons.firstMatch tap]; + XCTAssertTrue(self.app.staticTexts[@"是否返回?"].exists); + [self.app.buttons[@"继续编辑"] tap]; + XCTAssertTrue(self.app.textViews.firstMatch.qd_hasKeyboardFocus); + + [self.app.navigationBars.firstMatch.buttons.firstMatch tap]; + XCTAssertTrue(self.app.staticTexts[@"是否返回?"].exists); + [self.app.buttons[@"返回"] tap]; + XCTAssertFalse(self.app.navigationBars[@"拦截系统navBar返回按钮事件"].exists); +} + +- (void)tapGridButton:(NSString *)gridButtonString { + XCUIElement *navigationBar = self.app.navigationBars.firstMatch; + XCUIElementQuery *navigationButtons = navigationBar.buttons; + XCUIElement *backButton = [navigationButtons elementBoundByIndex:0]; + while (![navigationBar.identifier isEqualToString:@"QMUIKit"]) { + [backButton tap]; + } + [self.app.scrollViews.otherElements.buttons[gridButtonString] tap]; + XCTAssertTrue(self.app.navigationBars[gridButtonString].exists); +} + +@end diff --git a/QMUIKeyboard/Common/Common.h b/QMUIKeyboard/Common/Common.h new file mode 100644 index 00000000..7dc7530a --- /dev/null +++ b/QMUIKeyboard/Common/Common.h @@ -0,0 +1,12 @@ +// +// Runtime.h +// TestTabBar +// +// Created by MoLice on 2020/M/25. +// Copyright © 2020 MoLice. All rights reserved. +// + +#import +#import "Runtime.h" +#import "NSMethodSignature+QMUI.h" +#import "UIImage+QMUI.h" diff --git a/QMUIKeyboard/Common/NSMethodSignature+QMUI.h b/QMUIKeyboard/Common/NSMethodSignature+QMUI.h new file mode 100644 index 00000000..3f246e8c --- /dev/null +++ b/QMUIKeyboard/Common/NSMethodSignature+QMUI.h @@ -0,0 +1,37 @@ +/***** + * Tencent is pleased to support the open source community by making QMUI_iOS available. + * Copyright (C) 2016-2020 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. + *****/ +// +// NSMethodSignature+QMUI.h +// QMUIKit +// +// Created by MoLice on 2019/A/28. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSMethodSignature (QMUI) + +/** + 返回一个避免 crash 的方法签名,用于重写 methodSignatureForSelector: 时作为垫底的 return 方案 + */ +@property(nullable, class, nonatomic, readonly) NSMethodSignature *qmui_avoidExceptionSignature; + +/** + 以 NSString 格式返回当前 NSMethodSignature 的 typeEncoding,例如 v@: + */ +@property(nullable, nonatomic, copy, readonly) NSString *qmui_typeString; + +/** + 以 const char 格式返回当前 NSMethodSignature 的 typeEncoding,例如 v@: + */ +@property(nullable, nonatomic, readonly) const char *qmui_typeEncoding; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUIKeyboard/Common/NSMethodSignature+QMUI.m b/QMUIKeyboard/Common/NSMethodSignature+QMUI.m new file mode 100644 index 00000000..d9c22bd9 --- /dev/null +++ b/QMUIKeyboard/Common/NSMethodSignature+QMUI.m @@ -0,0 +1,43 @@ +/***** + * Tencent is pleased to support the open source community by making QMUI_iOS available. + * Copyright (C) 2016-2020 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. + *****/ +// +// NSMethodSignature+QMUI.m +// QMUIKit +// +// Created by MoLice on 2019/A/28. +// + +#import "NSMethodSignature+QMUI.h" + +@implementation NSMethodSignature (QMUI) + ++ (NSMethodSignature *)qmui_avoidExceptionSignature { + // https://github.com/facebookarchive/AsyncDisplayKit/pull/1562 + // Unfortunately, in order to get this object to work properly, the use of a method which creates an NSMethodSignature + // from a C string. -methodSignatureForSelector is called when a compiled definition for the selector cannot be found. + // This is the place where we have to create our own dud NSMethodSignature. This is necessary because if this method + // returns nil, a selector not found exception is raised. The string argument to -signatureWithObjCTypes: outlines + // the return type and arguments to the message. To return a dud NSMethodSignature, pretty much any signature will + // suffice. Since the -forwardInvocation call will do nothing if the delegate does not respond to the selector, + // the dud NSMethodSignature simply gets us around the exception. + return [NSMethodSignature signatureWithObjCTypes:"@^v^c"]; +} + +- (NSString *)qmui_typeString { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + NSString *typeString = [self performSelector:NSSelectorFromString([NSString stringWithFormat:@"_%@String", @"type"])]; +#pragma clang diagnostic pop + return typeString; +} + +- (const char *)qmui_typeEncoding { + return self.qmui_typeString.UTF8String; +} + +@end diff --git a/QMUIKeyboard/Common/Runtime.h b/QMUIKeyboard/Common/Runtime.h new file mode 100644 index 00000000..ee7dd3b3 --- /dev/null +++ b/QMUIKeyboard/Common/Runtime.h @@ -0,0 +1,65 @@ +// +// Runtime.h +// TestTabBar +// +// Created by MoLice on 2020/M/25. +// Copyright © 2020 MoLice. All rights reserved. +// + +#import +#import +#import "NSMethodSignature+QMUI.h" + +NS_ASSUME_NONNULL_BEGIN + +CG_INLINE BOOL +HasOverrideSuperclassMethod(Class targetClass, SEL targetSelector) { + Method method = class_getInstanceMethod(targetClass, targetSelector); + if (!method) return NO; + + Method methodOfSuperclass = class_getInstanceMethod(class_getSuperclass(targetClass), targetSelector); + if (!methodOfSuperclass) return YES; + + return method != methodOfSuperclass; +} + +CG_INLINE BOOL +OverrideImplementation(Class targetClass, SEL targetSelector, id (^implementationBlock)(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void))) { + Method originMethod = class_getInstanceMethod(targetClass, targetSelector); + IMP imp = method_getImplementation(originMethod); + BOOL hasOverride = HasOverrideSuperclassMethod(targetClass, targetSelector); + + // 以 block 的方式达到实时获取初始方法的 IMP 的目的,从而避免先 swizzle 了 subclass 的方法,再 swizzle superclass 的方法,会发现前者调用时不会触发后者 swizzle 后的版本的 bug。 + IMP (^originalIMPProvider)(void) = ^IMP(void) { + IMP result = NULL; + if (hasOverride) { + result = imp; + } else { + // 如果 superclass 里依然没有实现,则会返回一个 objc_msgForward 从而触发消息转发的流程 + // https://github.com/Tencent/QMUI_iOS/issues/776 + Class superclass = class_getSuperclass(targetClass); + result = class_getMethodImplementation(superclass, targetSelector); + } + + // 这只是一个保底,这里要返回一个空 block 保证非 nil,才能避免用小括号语法调用 block 时 crash + // 空 block 虽然没有参数列表,但在业务那边被转换成 IMP 后就算传多个参数进来也不会 crash + if (!result) { + result = imp_implementationWithBlock(^(id selfObject){ + NSLog(@"%@ 没有初始实现,%@\n%@", NSStringFromSelector(targetSelector), selfObject, [NSThread callStackSymbols]); + }); + } + + return result; + }; + + if (hasOverride) { + method_setImplementation(originMethod, imp_implementationWithBlock(implementationBlock(targetClass, targetSelector, originalIMPProvider))); + } else { + const char *typeEncoding = method_getTypeEncoding(originMethod) ?: [targetClass instanceMethodSignatureForSelector:targetSelector].qmui_typeEncoding; + class_addMethod(targetClass, targetSelector, imp_implementationWithBlock(implementationBlock(targetClass, targetSelector, originalIMPProvider)), typeEncoding); + } + + return YES; +} + +NS_ASSUME_NONNULL_END diff --git a/QMUIKeyboard/Common/UIImage+QMUI.h b/QMUIKeyboard/Common/UIImage+QMUI.h new file mode 100644 index 00000000..5f9ae46a --- /dev/null +++ b/QMUIKeyboard/Common/UIImage+QMUI.h @@ -0,0 +1,25 @@ +/***** + * Tencent is pleased to support the open source community by making QMUI_iOS available. + * Copyright (C) 2016-2020 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. + *****/ +// +// NSMethodSignature+QMUI.h +// QMUIKit +// +// Created by MoLice on 2019/A/28. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIImage (Test) + ++ (UIImage *)qmui_imageWithColor:(UIColor *)color size:(CGSize)size cornerRadius:(CGFloat)cornerRadius; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUIKeyboard/Common/UIImage+QMUI.m b/QMUIKeyboard/Common/UIImage+QMUI.m new file mode 100644 index 00000000..fcfe5714 --- /dev/null +++ b/QMUIKeyboard/Common/UIImage+QMUI.m @@ -0,0 +1,48 @@ +/***** + * Tencent is pleased to support the open source community by making QMUI_iOS available. + * Copyright (C) 2016-2020 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. + *****/ +// +// NSMethodSignature+QMUI.m +// QMUIKit +// +// Created by MoLice on 2019/A/28. +// + +#import "UIImage+QMUI.h" +#import "NSMethodSignature+QMUI.h" + +@implementation UIImage (Test) + ++ (UIImage *)qmui_imageWithSize:(CGSize)size opaque:(BOOL)opaque scale:(CGFloat)scale actions:(void (^)(CGContextRef contextRef))actionBlock { + if (!actionBlock) { + return nil; + } + UIGraphicsBeginImageContextWithOptions(size, opaque, scale); + CGContextRef context = UIGraphicsGetCurrentContext(); + actionBlock(context); + UIImage *imageOut = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return imageOut; +} + ++ (UIImage *)qmui_imageWithColor:(UIColor *)color size:(CGSize)size cornerRadius:(CGFloat)cornerRadius { + color = color ? color : UIColor.clearColor; + BOOL opaque = YES; + return [self qmui_imageWithSize:size opaque:opaque scale:0 actions:^(CGContextRef contextRef) { + CGContextSetFillColorWithColor(contextRef, color.CGColor); + + if (cornerRadius > 0) { + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, size.width, size.height) cornerRadius:cornerRadius]; + [path addClip]; + [path fill]; + } else { + CGContextFillRect(contextRef, CGRectMake(0, 0, size.width, size.height)); + } + }]; +} + +@end diff --git a/QMUIKeyboard/Info.plist b/QMUIKeyboard/Info.plist new file mode 100644 index 00000000..9f7c5662 --- /dev/null +++ b/QMUIKeyboard/Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + QMUIKeyboard + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionAttributes + + IsASCIICapable + + PrefersRightToLeft + + PrimaryLanguage + en-US + RequestsOpenAccess + + + NSExtensionPointIdentifier + com.apple.keyboard-service + NSExtensionPrincipalClass + KeyboardViewController + + + diff --git a/QMUIKeyboard/KeyboardViewController.h b/QMUIKeyboard/KeyboardViewController.h new file mode 100644 index 00000000..f11128b4 --- /dev/null +++ b/QMUIKeyboard/KeyboardViewController.h @@ -0,0 +1,13 @@ +// +// KeyboardViewController.h +// QMUIKeyboard +// +// Created by MoLice on 2021/A/31. +// Copyright © 2021 QMUI Team. All rights reserved. +// + +#import + +@interface KeyboardViewController : UIInputViewController + +@end diff --git a/QMUIKeyboard/KeyboardViewController.m b/QMUIKeyboard/KeyboardViewController.m new file mode 100644 index 00000000..b8e52f3d --- /dev/null +++ b/QMUIKeyboard/KeyboardViewController.m @@ -0,0 +1,103 @@ +// +// KeyboardViewController.m +// QMUIKeyboard +// +// Created by MoLice on 2021/A/31. +// Copyright © 2021 QMUI Team. All rights reserved. +// + +#import "KeyboardViewController.h" +#import "Common.h" + +@interface KeyboardViewController () +@property (nonatomic, strong) UIButton *nextKeyboardButton; +@property(nonatomic, strong) UILabel *countLabel; +@end + +@implementation KeyboardViewController + +static NSInteger count = 0; + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation([UIImage class], @selector(initWithCoder:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UIImage *selfObject, NSCoder *firstArgv) { + + // call super + UIImage * (*originSelectorIMP)(id, SEL, NSCoder *); + originSelectorIMP = (UIImage * (*)(id, SEL, NSCoder *))originalIMPProvider(); + UIImage * result = originSelectorIMP(selfObject, originCMD, firstArgv); + + NSLog(@"-[UIImage initWithCoder:], %@", result); + count++; + + return result; + }; + }); + }); +} + +- (void)updateViewConstraints { + [super updateViewConstraints]; + + // Add custom view sizing constraints here +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + // Perform custom UI setup here + self.nextKeyboardButton = [UIButton buttonWithType:UIButtonTypeSystem]; + + [self.nextKeyboardButton setTitle:NSLocalizedString(@"Next Keyboard", @"Title for 'Next Keyboard' button") forState:UIControlStateNormal]; + [self.nextKeyboardButton sizeToFit]; + self.nextKeyboardButton.translatesAutoresizingMaskIntoConstraints = NO; + + [self.nextKeyboardButton addTarget:self action:@selector(handleInputModeListFromView:withEvent:) forControlEvents:UIControlEventAllTouchEvents]; + + [self.view addSubview:self.nextKeyboardButton]; + + [self.nextKeyboardButton.leftAnchor constraintEqualToAnchor:self.view.leftAnchor].active = YES; + [self.nextKeyboardButton.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor].active = YES; + + self.countLabel = UILabel.new; + self.countLabel.textColor = [UIColor.blackColor colorWithAlphaComponent:.8]; + self.countLabel.font = [UIFont fontWithName:@"Menlo" size:16]; + self.countLabel.textAlignment = NSTextAlignmentCenter; + self.countLabel.numberOfLines = 0; + [self.view addSubview:self.countLabel]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + self.countLabel.text = [NSString stringWithFormat:@"-[UIImage initWithCoder:]\n%@次", @(count)]; + [self.view setNeedsLayout]; +} + +- (void)viewWillLayoutSubviews +{ + self.nextKeyboardButton.hidden = !self.needsInputModeSwitchKey; + [super viewWillLayoutSubviews]; + + [self.countLabel sizeToFit]; + self.countLabel.center = CGPointMake(CGRectGetWidth(self.view.bounds) / 2, CGRectGetHeight(self.view.bounds) / 2); +} + +- (void)textWillChange:(id)textInput { + // The app is about to change the document's contents. Perform any preparation here. +} + +- (void)textDidChange:(id)textInput { + // The app has just changed the document's contents, the document context has been updated. + + UIColor *textColor = nil; + if (self.textDocumentProxy.keyboardAppearance == UIKeyboardAppearanceDark) { + textColor = [UIColor whiteColor]; + } else { + textColor = [UIColor blackColor]; + } + [self.nextKeyboardButton setTitleColor:textColor forState:UIControlStateNormal]; +} + +@end diff --git a/README.md b/README.md index 3d0b9a17..13bf832d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,18 @@ # QMUIDemo_iOS Sample Code for QMUI iOS https://github.com/Tencent/QMUI_iOS -支持 iOS 版本:iOS 8.0+ +支持 iOS 版本:iOS 13.0+ -## 关于 GitHub 版本和 AppStore 版本 +## Sketch Files -之前 QMUI Demo 也同时发布在 AppStore 上,后来由于苹果审核原因,Demo 性质的 App 不允许上架,因此 AppStore 的版本停留在 1.7.2,短期内也无计划更新。若想体验最新版本的 QMUI Demo,请自行拉取 GitHub 项目代码运行。 +https://github.com/QMUI/QMUIDemo_Design + +## 内部维护方式 + +### 如果要在 QMUI 内新增文件 + +1. 在 Xcode 里创建完文件后,打开 qmui.xcodeproj -> Build Phases -> Headers,展开 Project,右键新增的头文件,选择“Move to Public Group”。如果该头文件是私有的(不想被外部直接使用)则不需要做这一步。 +2. 编译项目,此时会通过 `umbrellaHeaderFileCreator.py` 脚本自动生成新的 `QMUIKit.h`,里面会包含所有的 Public Headers。 +3. 如果你新增的文件属于 `QMUIComponents`,则需要编辑 QMUI 根目录下的 `QMUIKit.podspec` 文件,在 `QMUIComponents` 模块下增加新的子模块,格式和命名参考已有的即可。注意子模块本身需要声明,而别的模块如果使用了这个新的子模块,也需要添加对新模块的依赖(`dependency`)。如果你新增的文件不属于 `QMUIComponents` 则不需要做这一步。 +4. 在 QMUI 根目录下执行 `python3 add_license.py` 终端命令,以给所有的 QMUI 文件统一文件头的开源协议声明。 +5. 如果某个 API、功能在新设备发布时需要重新检查,请在该代码处加上“@NEW_DEVICE_CHECKER”的标志。 diff --git a/qmuidemo.xcodeproj/project.pbxproj b/qmuidemo.xcodeproj/project.pbxproj index a02a489b..63ee53c0 100644 --- a/qmuidemo.xcodeproj/project.pbxproj +++ b/qmuidemo.xcodeproj/project.pbxproj @@ -3,25 +3,11 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 60; objects = { -/* Begin PBXAggregateTarget section */ - CDE85CF01F9701D600E622E8 /* qmuidemo-rdm */ = { - isa = PBXAggregateTarget; - buildConfigurationList = CDE85CF11F9701D600E622E8 /* Build configuration list for PBXAggregateTarget "qmuidemo-rdm" */; - buildPhases = ( - CDE85CF71F9701E300E622E8 /* RDM */, - ); - dependencies = ( - CDE85CF61F9701E000E622E8 /* PBXTargetDependency */, - ); - name = "qmuidemo-rdm"; - productName = "qmuidemo-rdm"; - }; -/* End PBXAggregateTarget section */ - /* Begin PBXBuildFile section */ + 08ACAA7122D66747008530C5 /* QDLargeTitlesViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 08ACAA7022D66747008530C5 /* QDLargeTitlesViewController.m */; }; 165F42F61ADB60180057EF6A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 165F42F51ADB60180057EF6A /* main.m */; }; 165F42F91ADB60180057EF6A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 165F42F81ADB60180057EF6A /* AppDelegate.m */; }; 165F43011ADB60180057EF6A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 165F43001ADB60180057EF6A /* Images.xcassets */; }; @@ -32,15 +18,18 @@ 1679E3311DFFC4610072B8A1 /* QDCustomToastContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1679E3301DFFC4610072B8A1 /* QDCustomToastContentView.m */; }; 16F42C121B1C60A70038B28F /* image0@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 16F42C111B1C60A70038B28F /* image0@2x.png */; }; 4CD323992109B362007033C9 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4CD323972109B362007033C9 /* Localizable.strings */; }; + 8F732792262757E20051952C /* LookinServer.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F732791262757E20051952C /* LookinServer.xcframework */; platformFilter = ios; }; + 8F732793262757E20051952C /* LookinServer.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8F732791262757E20051952C /* LookinServer.xcframework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CD05B5E22743E1240001C5E0 /* QDAnimationCurvesViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD05B5E12743E1240001C5E0 /* QDAnimationCurvesViewController.m */; }; + CD05C20C2750D70F0001C5E0 /* QDBlurEffectViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD05C20B2750D70F0001C5E0 /* QDBlurEffectViewController.m */; }; + CD0ADF6927293961002A1A54 /* QMUIDropdownNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = CD0ADF6827293961002A1A54 /* QMUIDropdownNotification.m */; }; + CD0ADF6C2729999C002A1A54 /* QDDropdownNotificationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD0ADF6B2729999C002A1A54 /* QDDropdownNotificationViewController.m */; }; CD0C342221E5E25200B781AD /* QDDynamicHeightTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CD0C342121E5E25200B781AD /* QDDynamicHeightTableViewCell.m */; }; CD0C342521E5E2D800B781AD /* QDCellHeightCacheViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD0C342421E5E2D800B781AD /* QDCellHeightCacheViewController.m */; }; - CD1229A71DF677AC003B9649 /* QDImagePreviewExampleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD1229A61DF677AC003B9649 /* QDImagePreviewExampleViewController.m */; }; CD1229AF1DF678FB003B9649 /* QDImagePreviewViewController1.m in Sources */ = {isa = PBXBuildFile; fileRef = CD1229AE1DF678FB003B9649 /* QDImagePreviewViewController1.m */; }; CD1229B21DF67922003B9649 /* QDImagePreviewViewController2.m in Sources */ = {isa = PBXBuildFile; fileRef = CD1229B11DF67922003B9649 /* QDImagePreviewViewController2.m */; }; CD18BD8221870EFD00E2A4E6 /* QDNavigationBarScrollingAnimatorViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD18BD8121870EFD00E2A4E6 /* QDNavigationBarScrollingAnimatorViewController.m */; }; CD18BD8521871DCE00E2A4E6 /* QDNavigationBarScrollingSnapAnimatorViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD18BD8421871DCE00E2A4E6 /* QDNavigationBarScrollingSnapAnimatorViewController.m */; }; - CD18CF2320F76A3D00EED53C /* QDAAViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD18CF2220F76A3D00EED53C /* QDAAViewController.m */; }; - CD1A8A721EC3110300B81693 /* QDThemeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD1A8A711EC3110300B81693 /* QDThemeViewController.m */; }; CD1A8AA11EC4012400B81693 /* QMUIConfigurationTemplateGrass.m in Sources */ = {isa = PBXBuildFile; fileRef = CD1A8AA01EC4012400B81693 /* QMUIConfigurationTemplateGrass.m */; }; CD1A8AA41EC4045500B81693 /* QMUIConfigurationTemplatePinkRose.m in Sources */ = {isa = PBXBuildFile; fileRef = CD1A8AA31EC4045500B81693 /* QMUIConfigurationTemplatePinkRose.m */; }; CD2FEE5C2260F6AC00298BF5 /* QDNavigationBarMaxYViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD2FEE5B2260F6AC00298BF5 /* QDNavigationBarMaxYViewController.m */; }; @@ -54,20 +43,25 @@ CD4D339E1F010FD100FB81B0 /* QDStaticTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD4D339D1F010FD100FB81B0 /* QDStaticTableViewController.m */; }; CD4EA4B42275D58400A55066 /* animatedImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = CD4EA4B32275D58400A55066 /* animatedImage.gif */; }; CD4EA4B72275D5C200A55066 /* QDImageViewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD4EA4B62275D5C200A55066 /* QDImageViewViewController.m */; }; + CD4EA598228D87B000A55066 /* QMUIDemoUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = CD4EA597228D87B000A55066 /* QMUIDemoUITests.m */; }; + CD4EA5A8228E9FCE00A55066 /* QDUITestTools.m in Sources */ = {isa = PBXBuildFile; fileRef = CD4EA5A7228E9FCE00A55066 /* QDUITestTools.m */; }; CD5387761EE0096C00654A73 /* QDSliderViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD5387751EE0096C00654A73 /* QDSliderViewController.m */; }; + CD60DB4C2C5BBC65005109B3 /* QDPopupMenuViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD60DB4B2C5BBC65005109B3 /* QDPopupMenuViewController.m */; }; + CD60DB552C5BDBE9005109B3 /* QDCheckboxViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD60DB542C5BDBE9005109B3 /* QDCheckboxViewController.m */; }; CD6BE204205BF41500BE093E /* QDCellHeightKeyCacheViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD6BE202205BF41500BE093E /* QDCellHeightKeyCacheViewController.m */; }; CD6CC62D1EF7A6EA00602EDD /* QDTableViewCellAccessoryTypeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD6CC62C1EF7A6EA00602EDD /* QDTableViewCellAccessoryTypeViewController.m */; }; CD7593621EF7A380004B5819 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = CD7593601EF7A35B004B5819 /* libxml2.tbd */; }; CD7593631EF7A388004B5819 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD7593611EF7A362004B5819 /* Photos.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; CD7593651EF7A39A004B5819 /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD7593641EF7A391004B5819 /* MediaPlayer.framework */; }; - CD766F7E216B6A6D005155BD /* QDScrollAnimatorViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD766F7D216B6A6D005155BD /* QDScrollAnimatorViewController.m */; }; + CD7A9A1122C4BC6D0093DAB4 /* QDThemeExampleView.m in Sources */ = {isa = PBXBuildFile; fileRef = CD7A9A1022C4BC6D0093DAB4 /* QDThemeExampleView.m */; }; CD9207261DD49CD100AE32C0 /* QDFloatLayoutViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD9207251DD49CD100AE32C0 /* QDFloatLayoutViewController.m */; }; CD9D6EC6211064060004E222 /* QDCAAnimationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD9D6EC5211064060004E222 /* QDCAAnimationViewController.m */; }; - CDA05DA51FB18FC900606756 /* QDTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA05DA41FB18FC900606756 /* QDTableViewController.m */; }; CDA05DA81FB1913400606756 /* QDTableViewHeaderFooterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA05DA71FB1913400606756 /* QDTableViewHeaderFooterViewController.m */; }; CDA1CC951EC1BF5200AB8A0F /* QMUIConfigurationTemplateGrapefruit.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1CC941EC1BF5200AB8A0F /* QMUIConfigurationTemplateGrapefruit.m */; }; CDA1CC991EC1C34300AB8A0F /* QDThemeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA1CC981EC1C34300AB8A0F /* QDThemeManager.m */; }; CDA74F9C206BC1D000AE3830 /* QDMultipleDelegatesViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA74F9B206BC1D000AE3830 /* QDMultipleDelegatesViewController.m */; }; + CDA9537C2814460300D0FF0E /* LookinConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA9537A2814460300D0FF0E /* LookinConfig.m */; }; + CDB58B252B46238F002D4894 /* QDLayouterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB58B242B46238F002D4894 /* QDLayouterViewController.m */; }; CDB8C9D01DCC815A00769DF0 /* QMUIConfigurationTemplate.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9381DCC815A00769DF0 /* QMUIConfigurationTemplate.m */; }; CDB8C9D11DCC815A00769DF0 /* QDCommonGridViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C93B1DCC815A00769DF0 /* QDCommonGridViewController.m */; }; CDB8C9D21DCC815A00769DF0 /* QDCommonGroupListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C93D1DCC815A00769DF0 /* QDCommonGroupListViewController.m */; }; @@ -96,7 +90,6 @@ CDB8C9EC1DCC815A00769DF0 /* QDSaveVideoToSpecifiedAlbumViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9741DCC815A00769DF0 /* QDSaveVideoToSpecifiedAlbumViewController.m */; }; CDB8C9ED1DCC815A00769DF0 /* QDSingleImagePickerPreviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9761DCC815A00769DF0 /* QDSingleImagePickerPreviewViewController.m */; }; CDB8C9EE1DCC815A00769DF0 /* QDAllAnimationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C97A1DCC815A00769DF0 /* QDAllAnimationViewController.m */; }; - CDB8C9EF1DCC815A00769DF0 /* QDAnimationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C97C1DCC815A00769DF0 /* QDAnimationViewController.m */; }; CDB8C9F01DCC815A00769DF0 /* QDCAShapeLoadingViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C97E1DCC815A00769DF0 /* QDCAShapeLoadingViewController.m */; }; CDB8C9F71DCC815A00769DF0 /* QDReplicatorLayerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C98C1DCC815A00769DF0 /* QDReplicatorLayerViewController.m */; }; CDB8C9F81DCC815A00769DF0 /* QDRippleAnimationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C98E1DCC815A00769DF0 /* QDRippleAnimationViewController.m */; }; @@ -108,28 +101,24 @@ CDB8C9FE1DCC815A00769DF0 /* QDChangeNavBarStyleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C99B1DCC815A00769DF0 /* QDChangeNavBarStyleViewController.m */; }; CDB8C9FF1DCC815A00769DF0 /* QDCollectionStackDemoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C99D1DCC815A00769DF0 /* QDCollectionStackDemoViewController.m */; }; CDB8CA001DCC815A00769DF0 /* QDCollectionDemoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C99F1DCC815A00769DF0 /* QDCollectionDemoViewController.m */; }; - CDB8CA011DCC815A00769DF0 /* QDCollectionListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9A11DCC815A00769DF0 /* QDCollectionListViewController.m */; }; CDB8CA021DCC815A00769DF0 /* QDCollectionViewDemoCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9A31DCC815A00769DF0 /* QDCollectionViewDemoCell.m */; }; CDB8CA031DCC815A00769DF0 /* QDColorViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9A51DCC815A00769DF0 /* QDColorViewController.m */; }; - CDB8CA051DCC815A00769DF0 /* QDFillButtonViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9A91DCC815A00769DF0 /* QDFillButtonViewController.m */; }; CDB8CA061DCC815A00769DF0 /* QDFoldCollectionViewLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9AB1DCC815A00769DF0 /* QDFoldCollectionViewLayout.m */; }; - CDB8CA071DCC815A00769DF0 /* QDGhostButtonViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9AD1DCC815A00769DF0 /* QDGhostButtonViewController.m */; }; CDB8CA081DCC815A00769DF0 /* QDImageViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9AF1DCC815A00769DF0 /* QDImageViewController.m */; }; CDB8CA091DCC815A00769DF0 /* QDTableViewCellInsetsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9B11DCC815A00769DF0 /* QDTableViewCellInsetsViewController.m */; }; CDB8CA0A1DCC815A00769DF0 /* QDInterceptBackButtonEventViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9B31DCC815A00769DF0 /* QDInterceptBackButtonEventViewController.m */; }; CDB8CA0B1DCC815A00769DF0 /* QDLabelViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9B51DCC815A00769DF0 /* QDLabelViewController.m */; }; - CDB8CA0C1DCC815A00769DF0 /* QDLinkButtonViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9B71DCC815A00769DF0 /* QDLinkButtonViewController.m */; }; CDB8CA0D1DCC815A00769DF0 /* QDNavigationButtonViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9B91DCC815A00769DF0 /* QDNavigationButtonViewController.m */; }; CDB8CA0E1DCC815A00769DF0 /* QDNavigationListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9BB1DCC815A00769DF0 /* QDNavigationListViewController.m */; }; CDB8CA0F1DCC815A00769DF0 /* QDNormalButtonViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9BD1DCC815A00769DF0 /* QDNormalButtonViewController.m */; }; CDB8CA101DCC815A00769DF0 /* QDSearchViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9BF1DCC815A00769DF0 /* QDSearchViewController.m */; }; - CDB8CA121DCC815A00769DF0 /* QDTabBarItemViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9C31DCC815A00769DF0 /* QDTabBarItemViewController.m */; }; - CDB8CA131DCC815A00769DF0 /* QDTableViewCellViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9C51DCC815A00769DF0 /* QDTableViewCellViewController.m */; }; + CDB8CA121DCC815A00769DF0 /* QDTabBarDemoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9C31DCC815A00769DF0 /* QDTabBarDemoViewController.m */; }; CDB8CA141DCC815A00769DF0 /* QDTextFieldViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9C71DCC815A00769DF0 /* QDTextFieldViewController.m */; }; CDB8CA151DCC815A00769DF0 /* QDTextViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9C91DCC815A00769DF0 /* QDTextViewController.m */; }; CDB8CA161DCC815A00769DF0 /* QDToolBarButtonViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9CB1DCC815A00769DF0 /* QDToolBarButtonViewController.m */; }; CDB8CA171DCC815A00769DF0 /* QDUIKitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9CD1DCC815A00769DF0 /* QDUIKitViewController.m */; }; - CDB8CA181DCC815A00769DF0 /* QDUIViewQMUIViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9CF1DCC815A00769DF0 /* QDUIViewQMUIViewController.m */; }; + CDC73B452942344F00F1D584 /* QDTableViewCellReorderStyleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC73B442942344F00F1D584 /* QDTableViewCellReorderStyleViewController.m */; }; + CDCD270D2B8E41C400D3500A /* QDSheetPresentationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDCD270C2B8E41C400D3500A /* QDSheetPresentationViewController.m */; }; CDD0D81F1E0A88B600A38B96 /* image2@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = CDD0D81A1E0A88B600A38B96 /* image2@2x.png */; }; CDD0D8201E0A88B600A38B96 /* image3@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = CDD0D81B1E0A88B600A38B96 /* image3@2x.png */; }; CDD0D8211E0A88B600A38B96 /* image4@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = CDD0D81C1E0A88B600A38B96 /* image4@2x.png */; }; @@ -143,17 +132,51 @@ CDE85CB51F95E21500E622E8 /* QMUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD75935B1EF7A2EC004B5819 /* QMUIKit.framework */; }; CDE85CB61F95E21500E622E8 /* QMUIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CD75935B1EF7A2EC004B5819 /* QMUIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CDF79AAF1F16531100EE8DE9 /* QDButtonEdgeInsetsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDF79AAE1F16531100EE8DE9 /* QDButtonEdgeInsetsViewController.m */; }; - D021DE43205E87E700FFA408 /* QDCellKeyCacheViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D021DE42205E87E700FFA408 /* QDCellKeyCacheViewController.m */; }; + CDFD573D26DE132800603D1E /* KeyboardViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDFD573C26DE132800603D1E /* KeyboardViewController.m */; }; + CDFD574126DE132800603D1E /* QMUIKeyboard.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = CDFD573926DE132700603D1E /* QMUIKeyboard.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + CDFD575426DE1BA100603D1E /* UIImage+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDFD574E26DE1BA100603D1E /* UIImage+QMUI.m */; }; + CDFD575526DE1BA100603D1E /* NSMethodSignature+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDFD575326DE1BA100603D1E /* NSMethodSignature+QMUI.m */; }; + D004542F24B4A1C400F723E0 /* QDSearchBarViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D004542E24B4A1C400F723E0 /* QDSearchBarViewController.m */; }; + D004543224B6014B00F723E0 /* QDStyleSelectableTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D004543124B6014B00F723E0 /* QDStyleSelectableTableViewController.m */; }; + D00809C224C5C4AC00C50AA1 /* UINavigationItem+QMUIBottomAccessoryView.m in Sources */ = {isa = PBXBuildFile; fileRef = D00809C124C5C4AC00C50AA1 /* UINavigationItem+QMUIBottomAccessoryView.m */; }; + D00809C924C5E5AA00C50AA1 /* UINavigationBar+QMUISmoothEffect.m in Sources */ = {isa = PBXBuildFile; fileRef = D00809C624C5E5A900C50AA1 /* UINavigationBar+QMUISmoothEffect.m */; }; D021DE46205E889100FFA408 /* QDCellSizeKeyCacheViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D021DE45205E889100FFA408 /* QDCellSizeKeyCacheViewController.m */; }; D0283ECC20C2F2290090DD45 /* QDBadgeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D0283ECB20C2F2290090DD45 /* QDBadgeViewController.m */; }; + D02A61D824FFCC5000E670D0 /* QDControlViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D02A61D724FFCC5000E670D0 /* QDControlViewController.m */; }; D02F87F81EDBE8C300EE2CA8 /* QDFontViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D02F87F71EDBE8C300EE2CA8 /* QDFontViewController.m */; }; + D03102B924AB1DE30095C232 /* QDTableViewCellSeparatorInsetsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D03102B824AB1DE30095C232 /* QDTableViewCellSeparatorInsetsViewController.m */; }; + D03278342475367200DF8FF3 /* QMUIInteractiveDebugPanelItem.m in Sources */ = {isa = PBXBuildFile; fileRef = D03278332475367200DF8FF3 /* QMUIInteractiveDebugPanelItem.m */; }; + D032783924754F9400DF8FF3 /* QMUIInteractiveDebugger.m in Sources */ = {isa = PBXBuildFile; fileRef = D032783524754F9300DF8FF3 /* QMUIInteractiveDebugger.m */; }; + D032783A24754F9400DF8FF3 /* QMUIInteractiveDebugPanelViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D032783624754F9300DF8FF3 /* QMUIInteractiveDebugPanelViewController.m */; }; + D043E67A25783BA300D1E507 /* QMUIBackBarButton.m in Sources */ = {isa = PBXBuildFile; fileRef = D043E67825783BA300D1E507 /* QMUIBackBarButton.m */; }; + D05086B424CF383B00963BCE /* QDNavigationBarSmoothEffectViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D05086B324CF383B00963BCE /* QDNavigationBarSmoothEffectViewController.m */; }; + D05086B724CF567A00963BCE /* QDNavigationBottomAccessoryViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D05086B624CF567A00963BCE /* QDNavigationBottomAccessoryViewController.m */; }; D052631521EE080E00D4F57A /* QDConsoleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D052631421EE080E00D4F57A /* QDConsoleViewController.m */; }; + D062F65B22BCD4C700737AD2 /* QDThemeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D062F65922BCD4C700737AD2 /* QDThemeViewController.m */; }; D07610291EE292930048301B /* QDMarqueeLabelViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D07610281EE292930048301B /* QDMarqueeLabelViewController.m */; }; + D0911AC2249B11AE00112D88 /* QDInteractiveDebugViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D0911AC1249B11AE00112D88 /* QDInteractiveDebugViewController.m */; }; + D0AF7891257E6D2F006436A7 /* QDBackBarButtonViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D0AF7890257E6D2F006436A7 /* QDBackBarButtonViewController.m */; }; + D0B1856822C5016F002578CD /* QMUIConfigurationTemplateDark.m in Sources */ = {isa = PBXBuildFile; fileRef = D0B1856522C5016F002578CD /* QMUIConfigurationTemplateDark.m */; }; + D0BEFAA52484FEEC0006D1B9 /* QDInsetGroupedTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D0BEFAA42484FEEB0006D1B9 /* QDInsetGroupedTableViewController.m */; }; FE5803331E949E1100159380 /* QDKeyboardViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FE5803311E949E1100159380 /* QDKeyboardViewController.m */; }; FE775E3C1DF95728007A341F /* QDActivityIndicator.m in Sources */ = {isa = PBXBuildFile; fileRef = FE775E3A1DF95728007A341F /* QDActivityIndicator.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + CD4EA59A228D87B000A55066 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 165F42E81ADB60180057EF6A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 165F42EF1ADB60180057EF6A; + remoteInfo = qmuidemo; + }; + CD4EA5A0228D87B000A55066 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CD7593531EF7A2EC004B5819 /* qmui.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = CD4EA571228C401E00A55066; + remoteInfo = QMUIKitTests; + }; CD75935A1EF7A2EC004B5819 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = CD7593531EF7A2EC004B5819 /* qmui.xcodeproj */; @@ -168,12 +191,12 @@ remoteGlobalIDString = FE0AFAD01D82B9D8000D21D9; remoteInfo = QMUIKit; }; - CDE85CF51F9701E000E622E8 /* PBXContainerItemProxy */ = { + CDFD573F26DE132800603D1E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 165F42E81ADB60180057EF6A /* Project object */; proxyType = 1; - remoteGlobalIDString = 165F42EF1ADB60180057EF6A; - remoteInfo = qmuidemo; + remoteGlobalIDString = CDFD573826DE132700603D1E; + remoteInfo = QMUIKeyboard; }; /* End PBXContainerItemProxy section */ @@ -184,14 +207,28 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 8F732793262757E20051952C /* LookinServer.xcframework in Embed Frameworks */, CDE85CB61F95E21500E622E8 /* QMUIKit.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + CDFD574226DE132800603D1E /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + CDFD574126DE132800603D1E /* QMUIKeyboard.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 08ACAA6F22D66747008530C5 /* QDLargeTitlesViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDLargeTitlesViewController.h; sourceTree = ""; }; + 08ACAA7022D66747008530C5 /* QDLargeTitlesViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDLargeTitlesViewController.m; sourceTree = ""; }; 165F42F01ADB60180057EF6A /* qmuidemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = qmuidemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 165F42F51ADB60180057EF6A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 165F42F71ADB60180057EF6A /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; @@ -215,12 +252,19 @@ 4CD323982109B362007033C9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; 4CD3239A2109B364007033C9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 4CD3239B2109B366007033C9 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + 8F732791262757E20051952C /* LookinServer.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = LookinServer.xcframework; path = qmuidemo/Frameworks/LookinServer.xcframework; sourceTree = ""; }; + CD05B5E02743E1240001C5E0 /* QDAnimationCurvesViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDAnimationCurvesViewController.h; sourceTree = ""; }; + CD05B5E12743E1240001C5E0 /* QDAnimationCurvesViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDAnimationCurvesViewController.m; sourceTree = ""; }; + CD05C20A2750D70F0001C5E0 /* QDBlurEffectViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDBlurEffectViewController.h; sourceTree = ""; }; + CD05C20B2750D70F0001C5E0 /* QDBlurEffectViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDBlurEffectViewController.m; sourceTree = ""; }; + CD0ADF6727293961002A1A54 /* QMUIDropdownNotification.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIDropdownNotification.h; sourceTree = ""; }; + CD0ADF6827293961002A1A54 /* QMUIDropdownNotification.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIDropdownNotification.m; sourceTree = ""; }; + CD0ADF6A2729999C002A1A54 /* QDDropdownNotificationViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDDropdownNotificationViewController.h; sourceTree = ""; }; + CD0ADF6B2729999C002A1A54 /* QDDropdownNotificationViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDDropdownNotificationViewController.m; sourceTree = ""; }; CD0C342021E5E25200B781AD /* QDDynamicHeightTableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDDynamicHeightTableViewCell.h; sourceTree = ""; }; CD0C342121E5E25200B781AD /* QDDynamicHeightTableViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDDynamicHeightTableViewCell.m; sourceTree = ""; }; CD0C342321E5E2D800B781AD /* QDCellHeightCacheViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDCellHeightCacheViewController.h; sourceTree = ""; }; CD0C342421E5E2D800B781AD /* QDCellHeightCacheViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDCellHeightCacheViewController.m; sourceTree = ""; }; - CD1229A51DF677AC003B9649 /* QDImagePreviewExampleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDImagePreviewExampleViewController.h; sourceTree = ""; }; - CD1229A61DF677AC003B9649 /* QDImagePreviewExampleViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDImagePreviewExampleViewController.m; sourceTree = ""; }; CD1229AD1DF678FB003B9649 /* QDImagePreviewViewController1.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDImagePreviewViewController1.h; sourceTree = ""; }; CD1229AE1DF678FB003B9649 /* QDImagePreviewViewController1.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDImagePreviewViewController1.m; sourceTree = ""; }; CD1229B01DF67922003B9649 /* QDImagePreviewViewController2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDImagePreviewViewController2.h; sourceTree = ""; }; @@ -229,10 +273,6 @@ CD18BD8121870EFD00E2A4E6 /* QDNavigationBarScrollingAnimatorViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDNavigationBarScrollingAnimatorViewController.m; sourceTree = ""; }; CD18BD8321871DCE00E2A4E6 /* QDNavigationBarScrollingSnapAnimatorViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDNavigationBarScrollingSnapAnimatorViewController.h; sourceTree = ""; }; CD18BD8421871DCE00E2A4E6 /* QDNavigationBarScrollingSnapAnimatorViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDNavigationBarScrollingSnapAnimatorViewController.m; sourceTree = ""; }; - CD18CF2120F76A3D00EED53C /* QDAAViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDAAViewController.h; sourceTree = ""; }; - CD18CF2220F76A3D00EED53C /* QDAAViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDAAViewController.m; sourceTree = ""; }; - CD1A8A701EC3110300B81693 /* QDThemeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDThemeViewController.h; sourceTree = ""; }; - CD1A8A711EC3110300B81693 /* QDThemeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDThemeViewController.m; sourceTree = ""; }; CD1A8A9F1EC4012400B81693 /* QMUIConfigurationTemplateGrass.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIConfigurationTemplateGrass.h; sourceTree = ""; }; CD1A8AA01EC4012400B81693 /* QMUIConfigurationTemplateGrass.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIConfigurationTemplateGrass.m; sourceTree = ""; }; CD1A8AA21EC4045500B81693 /* QMUIConfigurationTemplatePinkRose.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIConfigurationTemplatePinkRose.h; sourceTree = ""; }; @@ -258,8 +298,18 @@ CD4EA4B32275D58400A55066 /* animatedImage.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = animatedImage.gif; sourceTree = ""; }; CD4EA4B52275D5C200A55066 /* QDImageViewViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDImageViewViewController.h; sourceTree = ""; }; CD4EA4B62275D5C200A55066 /* QDImageViewViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDImageViewViewController.m; sourceTree = ""; }; + CD4EA595228D87B000A55066 /* QMUIDemoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = QMUIDemoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + CD4EA597228D87B000A55066 /* QMUIDemoUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIDemoUITests.m; sourceTree = ""; }; + CD4EA599228D87B000A55066 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CD4EA5A6228E9FCE00A55066 /* QDUITestTools.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDUITestTools.h; sourceTree = ""; }; + CD4EA5A7228E9FCE00A55066 /* QDUITestTools.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDUITestTools.m; sourceTree = ""; }; + CD4EA5A9228EDBFF00A55066 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; CD5387741EE0096C00654A73 /* QDSliderViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDSliderViewController.h; sourceTree = ""; }; CD5387751EE0096C00654A73 /* QDSliderViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDSliderViewController.m; sourceTree = ""; }; + CD60DB4A2C5BBC65005109B3 /* QDPopupMenuViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDPopupMenuViewController.h; sourceTree = ""; }; + CD60DB4B2C5BBC65005109B3 /* QDPopupMenuViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDPopupMenuViewController.m; sourceTree = ""; }; + CD60DB532C5BDBE9005109B3 /* QDCheckboxViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDCheckboxViewController.h; sourceTree = ""; }; + CD60DB542C5BDBE9005109B3 /* QDCheckboxViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDCheckboxViewController.m; sourceTree = ""; }; CD6BE202205BF41500BE093E /* QDCellHeightKeyCacheViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDCellHeightKeyCacheViewController.m; sourceTree = ""; }; CD6BE203205BF41500BE093E /* QDCellHeightKeyCacheViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDCellHeightKeyCacheViewController.h; sourceTree = ""; }; CD6CC62B1EF7A6EA00602EDD /* QDTableViewCellAccessoryTypeViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDTableViewCellAccessoryTypeViewController.h; sourceTree = ""; }; @@ -268,14 +318,12 @@ CD7593601EF7A35B004B5819 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; }; CD7593611EF7A362004B5819 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; CD7593641EF7A391004B5819 /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; }; - CD766F7C216B6A6D005155BD /* QDScrollAnimatorViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDScrollAnimatorViewController.h; sourceTree = ""; }; - CD766F7D216B6A6D005155BD /* QDScrollAnimatorViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDScrollAnimatorViewController.m; sourceTree = ""; }; + CD7A9A0F22C4BC6D0093DAB4 /* QDThemeExampleView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDThemeExampleView.h; sourceTree = ""; }; + CD7A9A1022C4BC6D0093DAB4 /* QDThemeExampleView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDThemeExampleView.m; sourceTree = ""; }; CD9207241DD49CD100AE32C0 /* QDFloatLayoutViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDFloatLayoutViewController.h; sourceTree = ""; }; CD9207251DD49CD100AE32C0 /* QDFloatLayoutViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDFloatLayoutViewController.m; sourceTree = ""; }; CD9D6EC4211064060004E222 /* QDCAAnimationViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDCAAnimationViewController.h; sourceTree = ""; }; CD9D6EC5211064060004E222 /* QDCAAnimationViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDCAAnimationViewController.m; sourceTree = ""; }; - CDA05DA31FB18FC900606756 /* QDTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDTableViewController.h; sourceTree = ""; }; - CDA05DA41FB18FC900606756 /* QDTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDTableViewController.m; sourceTree = ""; }; CDA05DA61FB1913400606756 /* QDTableViewHeaderFooterViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDTableViewHeaderFooterViewController.h; sourceTree = ""; }; CDA05DA71FB1913400606756 /* QDTableViewHeaderFooterViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDTableViewHeaderFooterViewController.m; sourceTree = ""; }; CDA1CC931EC1BF5200AB8A0F /* QMUIConfigurationTemplateGrapefruit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIConfigurationTemplateGrapefruit.h; sourceTree = ""; }; @@ -285,6 +333,10 @@ CDA1CC9A1EC1C47800AB8A0F /* QDThemeProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDThemeProtocol.h; sourceTree = ""; }; CDA74F9A206BC1D000AE3830 /* QDMultipleDelegatesViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDMultipleDelegatesViewController.h; sourceTree = ""; }; CDA74F9B206BC1D000AE3830 /* QDMultipleDelegatesViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDMultipleDelegatesViewController.m; sourceTree = ""; }; + CDA953792814460300D0FF0E /* LookinConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LookinConfig.h; sourceTree = ""; }; + CDA9537A2814460300D0FF0E /* LookinConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LookinConfig.m; sourceTree = ""; }; + CDB58B232B46238E002D4894 /* QDLayouterViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDLayouterViewController.h; sourceTree = ""; }; + CDB58B242B46238F002D4894 /* QDLayouterViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDLayouterViewController.m; sourceTree = ""; }; CDB8C9371DCC815A00769DF0 /* QMUIConfigurationTemplate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIConfigurationTemplate.h; sourceTree = ""; }; CDB8C9381DCC815A00769DF0 /* QMUIConfigurationTemplate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIConfigurationTemplate.m; sourceTree = ""; }; CDB8C93A1DCC815A00769DF0 /* QDCommonGridViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDCommonGridViewController.h; sourceTree = ""; }; @@ -341,8 +393,6 @@ CDB8C9761DCC815A00769DF0 /* QDSingleImagePickerPreviewViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDSingleImagePickerPreviewViewController.m; sourceTree = ""; }; CDB8C9791DCC815A00769DF0 /* QDAllAnimationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDAllAnimationViewController.h; sourceTree = ""; }; CDB8C97A1DCC815A00769DF0 /* QDAllAnimationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDAllAnimationViewController.m; sourceTree = ""; }; - CDB8C97B1DCC815A00769DF0 /* QDAnimationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDAnimationViewController.h; sourceTree = ""; }; - CDB8C97C1DCC815A00769DF0 /* QDAnimationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDAnimationViewController.m; sourceTree = ""; }; CDB8C97D1DCC815A00769DF0 /* QDCAShapeLoadingViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDCAShapeLoadingViewController.h; sourceTree = ""; }; CDB8C97E1DCC815A00769DF0 /* QDCAShapeLoadingViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDCAShapeLoadingViewController.m; sourceTree = ""; }; CDB8C98B1DCC815A00769DF0 /* QDReplicatorLayerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDReplicatorLayerViewController.h; sourceTree = ""; }; @@ -365,18 +415,12 @@ CDB8C99D1DCC815A00769DF0 /* QDCollectionStackDemoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDCollectionStackDemoViewController.m; sourceTree = ""; }; CDB8C99E1DCC815A00769DF0 /* QDCollectionDemoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDCollectionDemoViewController.h; sourceTree = ""; }; CDB8C99F1DCC815A00769DF0 /* QDCollectionDemoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDCollectionDemoViewController.m; sourceTree = ""; }; - CDB8C9A01DCC815A00769DF0 /* QDCollectionListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDCollectionListViewController.h; sourceTree = ""; }; - CDB8C9A11DCC815A00769DF0 /* QDCollectionListViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDCollectionListViewController.m; sourceTree = ""; }; CDB8C9A21DCC815A00769DF0 /* QDCollectionViewDemoCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDCollectionViewDemoCell.h; sourceTree = ""; }; CDB8C9A31DCC815A00769DF0 /* QDCollectionViewDemoCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDCollectionViewDemoCell.m; sourceTree = ""; }; CDB8C9A41DCC815A00769DF0 /* QDColorViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDColorViewController.h; sourceTree = ""; }; CDB8C9A51DCC815A00769DF0 /* QDColorViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDColorViewController.m; sourceTree = ""; }; - CDB8C9A81DCC815A00769DF0 /* QDFillButtonViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDFillButtonViewController.h; sourceTree = ""; }; - CDB8C9A91DCC815A00769DF0 /* QDFillButtonViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDFillButtonViewController.m; sourceTree = ""; }; CDB8C9AA1DCC815A00769DF0 /* QDFoldCollectionViewLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDFoldCollectionViewLayout.h; sourceTree = ""; }; CDB8C9AB1DCC815A00769DF0 /* QDFoldCollectionViewLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDFoldCollectionViewLayout.m; sourceTree = ""; }; - CDB8C9AC1DCC815A00769DF0 /* QDGhostButtonViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDGhostButtonViewController.h; sourceTree = ""; }; - CDB8C9AD1DCC815A00769DF0 /* QDGhostButtonViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDGhostButtonViewController.m; sourceTree = ""; }; CDB8C9AE1DCC815A00769DF0 /* QDImageViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDImageViewController.h; sourceTree = ""; }; CDB8C9AF1DCC815A00769DF0 /* QDImageViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDImageViewController.m; sourceTree = ""; }; CDB8C9B01DCC815A00769DF0 /* QDTableViewCellInsetsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDTableViewCellInsetsViewController.h; sourceTree = ""; }; @@ -385,8 +429,6 @@ CDB8C9B31DCC815A00769DF0 /* QDInterceptBackButtonEventViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDInterceptBackButtonEventViewController.m; sourceTree = ""; }; CDB8C9B41DCC815A00769DF0 /* QDLabelViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDLabelViewController.h; sourceTree = ""; }; CDB8C9B51DCC815A00769DF0 /* QDLabelViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDLabelViewController.m; sourceTree = ""; }; - CDB8C9B61DCC815A00769DF0 /* QDLinkButtonViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDLinkButtonViewController.h; sourceTree = ""; }; - CDB8C9B71DCC815A00769DF0 /* QDLinkButtonViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDLinkButtonViewController.m; sourceTree = ""; }; CDB8C9B81DCC815A00769DF0 /* QDNavigationButtonViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDNavigationButtonViewController.h; sourceTree = ""; }; CDB8C9B91DCC815A00769DF0 /* QDNavigationButtonViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDNavigationButtonViewController.m; sourceTree = ""; }; CDB8C9BA1DCC815A00769DF0 /* QDNavigationListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDNavigationListViewController.h; sourceTree = ""; }; @@ -395,10 +437,8 @@ CDB8C9BD1DCC815A00769DF0 /* QDNormalButtonViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDNormalButtonViewController.m; sourceTree = ""; }; CDB8C9BE1DCC815A00769DF0 /* QDSearchViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDSearchViewController.h; sourceTree = ""; }; CDB8C9BF1DCC815A00769DF0 /* QDSearchViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDSearchViewController.m; sourceTree = ""; }; - CDB8C9C21DCC815A00769DF0 /* QDTabBarItemViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDTabBarItemViewController.h; sourceTree = ""; }; - CDB8C9C31DCC815A00769DF0 /* QDTabBarItemViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDTabBarItemViewController.m; sourceTree = ""; }; - CDB8C9C41DCC815A00769DF0 /* QDTableViewCellViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDTableViewCellViewController.h; sourceTree = ""; }; - CDB8C9C51DCC815A00769DF0 /* QDTableViewCellViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDTableViewCellViewController.m; sourceTree = ""; }; + CDB8C9C21DCC815A00769DF0 /* QDTabBarDemoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDTabBarDemoViewController.h; sourceTree = ""; }; + CDB8C9C31DCC815A00769DF0 /* QDTabBarDemoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDTabBarDemoViewController.m; sourceTree = ""; }; CDB8C9C61DCC815A00769DF0 /* QDTextFieldViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDTextFieldViewController.h; sourceTree = ""; }; CDB8C9C71DCC815A00769DF0 /* QDTextFieldViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDTextFieldViewController.m; sourceTree = ""; }; CDB8C9C81DCC815A00769DF0 /* QDTextViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDTextViewController.h; sourceTree = ""; }; @@ -407,8 +447,10 @@ CDB8C9CB1DCC815A00769DF0 /* QDToolBarButtonViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDToolBarButtonViewController.m; sourceTree = ""; }; CDB8C9CC1DCC815A00769DF0 /* QDUIKitViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDUIKitViewController.h; sourceTree = ""; }; CDB8C9CD1DCC815A00769DF0 /* QDUIKitViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDUIKitViewController.m; sourceTree = ""; }; - CDB8C9CE1DCC815A00769DF0 /* QDUIViewQMUIViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDUIViewQMUIViewController.h; sourceTree = ""; }; - CDB8C9CF1DCC815A00769DF0 /* QDUIViewQMUIViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDUIViewQMUIViewController.m; sourceTree = ""; }; + CDC73B432942344F00F1D584 /* QDTableViewCellReorderStyleViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDTableViewCellReorderStyleViewController.h; sourceTree = ""; }; + CDC73B442942344F00F1D584 /* QDTableViewCellReorderStyleViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDTableViewCellReorderStyleViewController.m; sourceTree = ""; }; + CDCD270B2B8E41C400D3500A /* QDSheetPresentationViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDSheetPresentationViewController.h; sourceTree = ""; }; + CDCD270C2B8E41C400D3500A /* QDSheetPresentationViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDSheetPresentationViewController.m; sourceTree = ""; }; CDD0D81A1E0A88B600A38B96 /* image2@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "image2@2x.png"; sourceTree = ""; }; CDD0D81B1E0A88B600A38B96 /* image3@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "image3@2x.png"; sourceTree = ""; }; CDD0D81C1E0A88B600A38B96 /* image4@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "image4@2x.png"; sourceTree = ""; }; @@ -424,18 +466,61 @@ CDE50ADE1DAA7059000D5414 /* libstdc++.6.0.9.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libstdc++.6.0.9.tbd"; path = "usr/lib/libstdc++.6.0.9.tbd"; sourceTree = SDKROOT; }; CDF79AAD1F16531100EE8DE9 /* QDButtonEdgeInsetsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDButtonEdgeInsetsViewController.h; sourceTree = ""; }; CDF79AAE1F16531100EE8DE9 /* QDButtonEdgeInsetsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDButtonEdgeInsetsViewController.m; sourceTree = ""; }; - D021DE41205E87E700FFA408 /* QDCellKeyCacheViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDCellKeyCacheViewController.h; sourceTree = ""; }; - D021DE42205E87E700FFA408 /* QDCellKeyCacheViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDCellKeyCacheViewController.m; sourceTree = ""; }; + CDFD573926DE132700603D1E /* QMUIKeyboard.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QMUIKeyboard.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + CDFD573B26DE132800603D1E /* KeyboardViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyboardViewController.h; sourceTree = ""; }; + CDFD573C26DE132800603D1E /* KeyboardViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KeyboardViewController.m; sourceTree = ""; }; + CDFD573E26DE132800603D1E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CDFD574E26DE1BA100603D1E /* UIImage+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+QMUI.m"; sourceTree = ""; }; + CDFD574F26DE1BA100603D1E /* NSMethodSignature+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMethodSignature+QMUI.h"; sourceTree = ""; }; + CDFD575026DE1BA100603D1E /* Runtime.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Runtime.h; sourceTree = ""; }; + CDFD575126DE1BA100603D1E /* UIImage+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+QMUI.h"; sourceTree = ""; }; + CDFD575226DE1BA100603D1E /* Common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Common.h; sourceTree = ""; }; + CDFD575326DE1BA100603D1E /* NSMethodSignature+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMethodSignature+QMUI.m"; sourceTree = ""; }; + CDFE9573293FA58A007AE1AA /* qmuidemo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = qmuidemo.entitlements; sourceTree = ""; }; + D004542D24B4A1C400F723E0 /* QDSearchBarViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDSearchBarViewController.h; sourceTree = ""; }; + D004542E24B4A1C400F723E0 /* QDSearchBarViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDSearchBarViewController.m; sourceTree = ""; }; + D004543024B6014B00F723E0 /* QDStyleSelectableTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDStyleSelectableTableViewController.h; sourceTree = ""; }; + D004543124B6014B00F723E0 /* QDStyleSelectableTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDStyleSelectableTableViewController.m; sourceTree = ""; }; + D00809C024C5C4AC00C50AA1 /* UINavigationItem+QMUIBottomAccessoryView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UINavigationItem+QMUIBottomAccessoryView.h"; sourceTree = ""; }; + D00809C124C5C4AC00C50AA1 /* UINavigationItem+QMUIBottomAccessoryView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UINavigationItem+QMUIBottomAccessoryView.m"; sourceTree = ""; }; + D00809C624C5E5A900C50AA1 /* UINavigationBar+QMUISmoothEffect.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UINavigationBar+QMUISmoothEffect.m"; sourceTree = ""; }; + D00809C724C5E5A900C50AA1 /* UINavigationBar+QMUISmoothEffect.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UINavigationBar+QMUISmoothEffect.h"; sourceTree = ""; }; D021DE44205E889100FFA408 /* QDCellSizeKeyCacheViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDCellSizeKeyCacheViewController.h; sourceTree = ""; }; D021DE45205E889100FFA408 /* QDCellSizeKeyCacheViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDCellSizeKeyCacheViewController.m; sourceTree = ""; }; D0283ECA20C2F2290090DD45 /* QDBadgeViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDBadgeViewController.h; sourceTree = ""; }; D0283ECB20C2F2290090DD45 /* QDBadgeViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDBadgeViewController.m; sourceTree = ""; }; + D02A61D624FFCC5000E670D0 /* QDControlViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDControlViewController.h; sourceTree = ""; }; + D02A61D724FFCC5000E670D0 /* QDControlViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDControlViewController.m; sourceTree = ""; }; D02F87F61EDBE8C200EE2CA8 /* QDFontViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDFontViewController.h; sourceTree = ""; }; D02F87F71EDBE8C300EE2CA8 /* QDFontViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDFontViewController.m; sourceTree = ""; }; + D03102B724AB1DE30095C232 /* QDTableViewCellSeparatorInsetsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDTableViewCellSeparatorInsetsViewController.h; sourceTree = ""; }; + D03102B824AB1DE30095C232 /* QDTableViewCellSeparatorInsetsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDTableViewCellSeparatorInsetsViewController.m; sourceTree = ""; }; + D03278322475367200DF8FF3 /* QMUIInteractiveDebugPanelItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIInteractiveDebugPanelItem.h; sourceTree = ""; }; + D03278332475367200DF8FF3 /* QMUIInteractiveDebugPanelItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIInteractiveDebugPanelItem.m; sourceTree = ""; }; + D032783524754F9300DF8FF3 /* QMUIInteractiveDebugger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIInteractiveDebugger.m; sourceTree = ""; }; + D032783624754F9300DF8FF3 /* QMUIInteractiveDebugPanelViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIInteractiveDebugPanelViewController.m; sourceTree = ""; }; + D032783724754F9300DF8FF3 /* QMUIInteractiveDebugger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIInteractiveDebugger.h; sourceTree = ""; }; + D032783824754F9300DF8FF3 /* QMUIInteractiveDebugPanelViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIInteractiveDebugPanelViewController.h; sourceTree = ""; }; + D043E67725783BA300D1E507 /* QMUIBackBarButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIBackBarButton.h; sourceTree = ""; }; + D043E67825783BA300D1E507 /* QMUIBackBarButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIBackBarButton.m; sourceTree = ""; }; + D05086B224CF383B00963BCE /* QDNavigationBarSmoothEffectViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDNavigationBarSmoothEffectViewController.h; sourceTree = ""; }; + D05086B324CF383B00963BCE /* QDNavigationBarSmoothEffectViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDNavigationBarSmoothEffectViewController.m; sourceTree = ""; }; + D05086B524CF567A00963BCE /* QDNavigationBottomAccessoryViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDNavigationBottomAccessoryViewController.h; sourceTree = ""; }; + D05086B624CF567A00963BCE /* QDNavigationBottomAccessoryViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDNavigationBottomAccessoryViewController.m; sourceTree = ""; }; D052631321EE080E00D4F57A /* QDConsoleViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDConsoleViewController.h; sourceTree = ""; }; D052631421EE080E00D4F57A /* QDConsoleViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDConsoleViewController.m; sourceTree = ""; }; + D062F65922BCD4C700737AD2 /* QDThemeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDThemeViewController.m; sourceTree = ""; }; + D062F65A22BCD4C700737AD2 /* QDThemeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDThemeViewController.h; sourceTree = ""; }; D07610271EE292930048301B /* QDMarqueeLabelViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDMarqueeLabelViewController.h; sourceTree = ""; }; D07610281EE292930048301B /* QDMarqueeLabelViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDMarqueeLabelViewController.m; sourceTree = ""; }; + D0911AC0249B11AE00112D88 /* QDInteractiveDebugViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDInteractiveDebugViewController.h; sourceTree = ""; }; + D0911AC1249B11AE00112D88 /* QDInteractiveDebugViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDInteractiveDebugViewController.m; sourceTree = ""; }; + D0AF788F257E6D2F006436A7 /* QDBackBarButtonViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDBackBarButtonViewController.h; sourceTree = ""; }; + D0AF7890257E6D2F006436A7 /* QDBackBarButtonViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDBackBarButtonViewController.m; sourceTree = ""; }; + D0B1856522C5016F002578CD /* QMUIConfigurationTemplateDark.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIConfigurationTemplateDark.m; sourceTree = ""; }; + D0B1856722C5016F002578CD /* QMUIConfigurationTemplateDark.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIConfigurationTemplateDark.h; sourceTree = ""; }; + D0BEFAA32484FEEB0006D1B9 /* QDInsetGroupedTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDInsetGroupedTableViewController.h; sourceTree = ""; }; + D0BEFAA42484FEEB0006D1B9 /* QDInsetGroupedTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDInsetGroupedTableViewController.m; sourceTree = ""; }; FE5803311E949E1100159380 /* QDKeyboardViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDKeyboardViewController.m; sourceTree = ""; }; FE5803321E949E1100159380 /* QDKeyboardViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDKeyboardViewController.h; sourceTree = ""; }; FE775E3A1DF95728007A341F /* QDActivityIndicator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDActivityIndicator.m; sourceTree = ""; }; @@ -452,12 +537,27 @@ CD7593631EF7A388004B5819 /* Photos.framework in Frameworks */, CD7593621EF7A380004B5819 /* libxml2.tbd in Frameworks */, CDE50ADD1DAA6EEA000D5414 /* CFNetwork.framework in Frameworks */, + 8F732792262757E20051952C /* LookinServer.xcframework in Frameworks */, CDE50ADB1DAA6EE5000D5414 /* SystemConfiguration.framework in Frameworks */, CDE50AD91DAA6ED9000D5414 /* Security.framework in Frameworks */, CDE50AD31DAA6E6F000D5414 /* libz.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; + CD4EA592228D87B000A55066 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CDFD573626DE132700603D1E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -465,6 +565,8 @@ isa = PBXGroup; children = ( 165F42F21ADB60180057EF6A /* qmuidemo */, + CD4EA596228D87B000A55066 /* QMUIDemoUITests */, + CDFD573A26DE132700603D1E /* QMUIKeyboard */, 165F42F11ADB60180057EF6A /* Products */, CDE50AD11DAA6E6F000D5414 /* Frameworks */, ); @@ -474,6 +576,8 @@ isa = PBXGroup; children = ( 165F42F01ADB60180057EF6A /* qmuidemo.app */, + CD4EA595228D87B000A55066 /* QMUIDemoUITests.xctest */, + CDFD573926DE132700603D1E /* QMUIKeyboard.appex */, ); name = Products; sourceTree = ""; @@ -481,6 +585,7 @@ 165F42F21ADB60180057EF6A /* qmuidemo */ = { isa = PBXGroup; children = ( + CDFE9573293FA58A007AE1AA /* qmuidemo.entitlements */, 165F42F71ADB60180057EF6A /* AppDelegate.h */, 165F42F81ADB60180057EF6A /* AppDelegate.m */, 165F43001ADB60180057EF6A /* Images.xcassets */, @@ -536,6 +641,26 @@ path = images; sourceTree = ""; }; + CD0ADF662729394A002A1A54 /* QMUIDropdownNotification */ = { + isa = PBXGroup; + children = ( + CD0ADF6727293961002A1A54 /* QMUIDropdownNotification.h */, + CD0ADF6827293961002A1A54 /* QMUIDropdownNotification.m */, + ); + path = QMUIDropdownNotification; + sourceTree = ""; + }; + CD4EA596228D87B000A55066 /* QMUIDemoUITests */ = { + isa = PBXGroup; + children = ( + CD4EA597228D87B000A55066 /* QMUIDemoUITests.m */, + CD4EA599228D87B000A55066 /* Info.plist */, + CD4EA5A6228E9FCE00A55066 /* QDUITestTools.h */, + CD4EA5A7228E9FCE00A55066 /* QDUITestTools.m */, + ); + path = QMUIDemoUITests; + sourceTree = ""; + }; CD75934F1EF7A25C004B5819 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -548,15 +673,26 @@ isa = PBXGroup; children = ( CD75935B1EF7A2EC004B5819 /* QMUIKit.framework */, + CD4EA5A1228D87B000A55066 /* QMUIKitTests.xctest */, ); name = Products; sourceTree = ""; }; + CDA953782814460300D0FF0E /* LookinConfig */ = { + isa = PBXGroup; + children = ( + CDA953792814460300D0FF0E /* LookinConfig.h */, + CDA9537A2814460300D0FF0E /* LookinConfig.m */, + ); + path = LookinConfig; + sourceTree = ""; + }; CDB8C9351DCC815A00769DF0 /* Common */ = { isa = PBXGroup; children = ( CDB8C9361DCC815A00769DF0 /* Configuration */, CDB8C9391DCC815A00769DF0 /* Controllers */, + CDA953782814460300D0FF0E /* LookinConfig */, CDB8C9481DCC815A00769DF0 /* Utils */, ); path = Common; @@ -570,6 +706,8 @@ CDA1CC9A1EC1C47800AB8A0F /* QDThemeProtocol.h */, CDB8C9371DCC815A00769DF0 /* QMUIConfigurationTemplate.h */, CDB8C9381DCC815A00769DF0 /* QMUIConfigurationTemplate.m */, + D0B1856722C5016F002578CD /* QMUIConfigurationTemplateDark.h */, + D0B1856522C5016F002578CD /* QMUIConfigurationTemplateDark.m */, CDA1CC931EC1BF5200AB8A0F /* QMUIConfigurationTemplateGrapefruit.h */, CDA1CC941EC1BF5200AB8A0F /* QMUIConfigurationTemplateGrapefruit.m */, CD1A8A9F1EC4012400B81693 /* QMUIConfigurationTemplateGrass.h */, @@ -597,6 +735,8 @@ CDB8C9451DCC815A00769DF0 /* QDNavigationController.m */, CDB8C9461DCC815A00769DF0 /* QDTabBarViewController.h */, CDB8C9471DCC815A00769DF0 /* QDTabBarViewController.m */, + D004543024B6014B00F723E0 /* QDStyleSelectableTableViewController.h */, + D004543124B6014B00F723E0 /* QDStyleSelectableTableViewController.m */, ); path = Controllers; sourceTree = ""; @@ -628,14 +768,6 @@ CDB8C9501DCC815A00769DF0 /* Components */ = { isa = PBXGroup; children = ( - CDB8C99E1DCC815A00769DF0 /* QDCollectionDemoViewController.h */, - CDB8C99F1DCC815A00769DF0 /* QDCollectionDemoViewController.m */, - CDB8C9A01DCC815A00769DF0 /* QDCollectionListViewController.h */, - CDB8C9A11DCC815A00769DF0 /* QDCollectionListViewController.m */, - CDB8C99C1DCC815A00769DF0 /* QDCollectionStackDemoViewController.h */, - CDB8C99D1DCC815A00769DF0 /* QDCollectionStackDemoViewController.m */, - CDB8C9A21DCC815A00769DF0 /* QDCollectionViewDemoCell.h */, - CDB8C9A31DCC815A00769DF0 /* QDCollectionViewDemoCell.m */, CDB8C9511DCC815A00769DF0 /* QDAssetsManagerViewController.h */, CDB8C9521DCC815A00769DF0 /* QDAssetsManagerViewController.m */, D0283ECA20C2F2290090DD45 /* QDBadgeViewController.h */, @@ -644,10 +776,14 @@ CD0C342421E5E2D800B781AD /* QDCellHeightCacheViewController.m */, CD6BE203205BF41500BE093E /* QDCellHeightKeyCacheViewController.h */, CD6BE202205BF41500BE093E /* QDCellHeightKeyCacheViewController.m */, - D021DE41205E87E700FFA408 /* QDCellKeyCacheViewController.h */, - D021DE42205E87E700FFA408 /* QDCellKeyCacheViewController.m */, D021DE44205E889100FFA408 /* QDCellSizeKeyCacheViewController.h */, D021DE45205E889100FFA408 /* QDCellSizeKeyCacheViewController.m */, + CDB8C99E1DCC815A00769DF0 /* QDCollectionDemoViewController.h */, + CDB8C99F1DCC815A00769DF0 /* QDCollectionDemoViewController.m */, + CDB8C99C1DCC815A00769DF0 /* QDCollectionStackDemoViewController.h */, + CDB8C99D1DCC815A00769DF0 /* QDCollectionStackDemoViewController.m */, + CDB8C9A21DCC815A00769DF0 /* QDCollectionViewDemoCell.h */, + CDB8C9A31DCC815A00769DF0 /* QDCollectionViewDemoCell.m */, CDB8C9531DCC815A00769DF0 /* QDComponentsViewController.h */, CDB8C9541DCC815A00769DF0 /* QDComponentsViewController.m */, D052631321EE080E00D4F57A /* QDConsoleViewController.h */, @@ -670,14 +806,14 @@ CDB8C95C1DCC815A00769DF0 /* QDGridViewController.m */, CDB8C95D1DCC815A00769DF0 /* QDImagePickerExampleViewController.h */, CDB8C95E1DCC815A00769DF0 /* QDImagePickerExampleViewController.m */, - CD1229A51DF677AC003B9649 /* QDImagePreviewExampleViewController.h */, - CD1229A61DF677AC003B9649 /* QDImagePreviewExampleViewController.m */, CD1229AD1DF678FB003B9649 /* QDImagePreviewViewController1.h */, CD1229AE1DF678FB003B9649 /* QDImagePreviewViewController1.m */, CD1229B01DF67922003B9649 /* QDImagePreviewViewController2.h */, CD1229B11DF67922003B9649 /* QDImagePreviewViewController2.m */, FE5803321E949E1100159380 /* QDKeyboardViewController.h */, FE5803311E949E1100159380 /* QDKeyboardViewController.m */, + CDB58B232B46238E002D4894 /* QDLayouterViewController.h */, + CDB58B242B46238F002D4894 /* QDLayouterViewController.m */, D07610271EE292930048301B /* QDMarqueeLabelViewController.h */, D07610281EE292930048301B /* QDMarqueeLabelViewController.m */, CDB8C95F1DCC815A00769DF0 /* QDModalPresentationViewController.h */, @@ -702,14 +838,22 @@ CDB8C9721DCC815A00769DF0 /* QDSaveImageToSpecifiedAlbumViewController.m */, CDB8C9731DCC815A00769DF0 /* QDSaveVideoToSpecifiedAlbumViewController.h */, CDB8C9741DCC815A00769DF0 /* QDSaveVideoToSpecifiedAlbumViewController.m */, - CD766F7C216B6A6D005155BD /* QDScrollAnimatorViewController.h */, - CD766F7D216B6A6D005155BD /* QDScrollAnimatorViewController.m */, + CDCD270B2B8E41C400D3500A /* QDSheetPresentationViewController.h */, + CDCD270C2B8E41C400D3500A /* QDSheetPresentationViewController.m */, CDB8C9751DCC815A00769DF0 /* QDSingleImagePickerPreviewViewController.h */, CDB8C9761DCC815A00769DF0 /* QDSingleImagePickerPreviewViewController.m */, CD4D339C1F010FD100FB81B0 /* QDStaticTableViewController.h */, CD4D339D1F010FD100FB81B0 /* QDStaticTableViewController.m */, + CD7A9A0F22C4BC6D0093DAB4 /* QDThemeExampleView.h */, + CD7A9A1022C4BC6D0093DAB4 /* QDThemeExampleView.m */, + D062F65A22BCD4C700737AD2 /* QDThemeViewController.h */, + D062F65922BCD4C700737AD2 /* QDThemeViewController.m */, 1679E3241DFF9E300072B8A1 /* QDToastListViewController.h */, 1679E3251DFF9E300072B8A1 /* QDToastListViewController.m */, + CD60DB4A2C5BBC65005109B3 /* QDPopupMenuViewController.h */, + CD60DB4B2C5BBC65005109B3 /* QDPopupMenuViewController.m */, + CD60DB532C5BDBE9005109B3 /* QDCheckboxViewController.h */, + CD60DB542C5BDBE9005109B3 /* QDCheckboxViewController.m */, ); path = Components; sourceTree = ""; @@ -720,12 +864,25 @@ CDB8C9781DCC815A00769DF0 /* Animation */, CDB8C98F1DCC815A00769DF0 /* QDAllSystemFontsViewController.h */, CDB8C9901DCC815A00769DF0 /* QDAllSystemFontsViewController.m */, + D0AF788F257E6D2F006436A7 /* QDBackBarButtonViewController.h */, + D0AF7890257E6D2F006436A7 /* QDBackBarButtonViewController.m */, + CD0ADF6A2729999C002A1A54 /* QDDropdownNotificationViewController.h */, + CD0ADF6B2729999C002A1A54 /* QDDropdownNotificationViewController.m */, CDB8C9911DCC815A00769DF0 /* QDFontPointSizeAndLineHeightViewController.h */, CDB8C9921DCC815A00769DF0 /* QDFontPointSizeAndLineHeightViewController.m */, + D0911AC0249B11AE00112D88 /* QDInteractiveDebugViewController.h */, + D0911AC1249B11AE00112D88 /* QDInteractiveDebugViewController.m */, CDB8C9931DCC815A00769DF0 /* QDLabViewController.h */, CDB8C9941DCC815A00769DF0 /* QDLabViewController.m */, - CD1A8A701EC3110300B81693 /* QDThemeViewController.h */, - CD1A8A711EC3110300B81693 /* QDThemeViewController.m */, + D05086B224CF383B00963BCE /* QDNavigationBarSmoothEffectViewController.h */, + D05086B324CF383B00963BCE /* QDNavigationBarSmoothEffectViewController.m */, + D05086B524CF567A00963BCE /* QDNavigationBottomAccessoryViewController.h */, + D05086B624CF567A00963BCE /* QDNavigationBottomAccessoryViewController.m */, + D043E67625783BA300D1E507 /* QMUIBackBarButton */, + CD0ADF662729394A002A1A54 /* QMUIDropdownNotification */, + D03278312475365E00DF8FF3 /* QMUIInteractiveDebugger */, + D00809C524C5E5A900C50AA1 /* QMUINavigationBarSmoothEffect */, + D00809BE24C5C48C00C50AA1 /* QMUINavigationBottomAccessoryView */, ); path = Lab; sourceTree = ""; @@ -737,8 +894,8 @@ FE775E3A1DF95728007A341F /* QDActivityIndicator.m */, CDB8C9791DCC815A00769DF0 /* QDAllAnimationViewController.h */, CDB8C97A1DCC815A00769DF0 /* QDAllAnimationViewController.m */, - CDB8C97B1DCC815A00769DF0 /* QDAnimationViewController.h */, - CDB8C97C1DCC815A00769DF0 /* QDAnimationViewController.m */, + CD05B5E02743E1240001C5E0 /* QDAnimationCurvesViewController.h */, + CD05B5E12743E1240001C5E0 /* QDAnimationCurvesViewController.m */, CDB8C97D1DCC815A00769DF0 /* QDCAShapeLoadingViewController.h */, CDB8C97E1DCC815A00769DF0 /* QDCAShapeLoadingViewController.m */, CDB8C98B1DCC815A00769DF0 /* QDReplicatorLayerViewController.h */, @@ -754,6 +911,8 @@ children = ( CDB8C9961DCC815A00769DF0 /* QDAlertController.h */, CDB8C9971DCC815A00769DF0 /* QDAlertController.m */, + CD05C20A2750D70F0001C5E0 /* QDBlurEffectViewController.h */, + CD05C20B2750D70F0001C5E0 /* QDBlurEffectViewController.m */, CDF79AAD1F16531100EE8DE9 /* QDButtonEdgeInsetsViewController.h */, CDF79AAE1F16531100EE8DE9 /* QDButtonEdgeInsetsViewController.m */, CDB8C9981DCC815A00769DF0 /* QDButtonViewController.h */, @@ -764,22 +923,26 @@ CDB8C99B1DCC815A00769DF0 /* QDChangeNavBarStyleViewController.m */, CDB8C9A41DCC815A00769DF0 /* QDColorViewController.h */, CDB8C9A51DCC815A00769DF0 /* QDColorViewController.m */, - CDB8C9A81DCC815A00769DF0 /* QDFillButtonViewController.h */, - CDB8C9A91DCC815A00769DF0 /* QDFillButtonViewController.m */, + D02A61D624FFCC5000E670D0 /* QDControlViewController.h */, + D02A61D724FFCC5000E670D0 /* QDControlViewController.m */, CDB8C9AA1DCC815A00769DF0 /* QDFoldCollectionViewLayout.h */, CDB8C9AB1DCC815A00769DF0 /* QDFoldCollectionViewLayout.m */, D02F87F61EDBE8C200EE2CA8 /* QDFontViewController.h */, D02F87F71EDBE8C300EE2CA8 /* QDFontViewController.m */, - CDB8C9AC1DCC815A00769DF0 /* QDGhostButtonViewController.h */, - CDB8C9AD1DCC815A00769DF0 /* QDGhostButtonViewController.m */, CDB8C9AE1DCC815A00769DF0 /* QDImageViewController.h */, CDB8C9AF1DCC815A00769DF0 /* QDImageViewController.m */, + CD4EA4B52275D5C200A55066 /* QDImageViewViewController.h */, + CD4EA4B62275D5C200A55066 /* QDImageViewViewController.m */, + D0BEFAA32484FEEB0006D1B9 /* QDInsetGroupedTableViewController.h */, + D0BEFAA42484FEEB0006D1B9 /* QDInsetGroupedTableViewController.m */, CDB8C9B21DCC815A00769DF0 /* QDInterceptBackButtonEventViewController.h */, CDB8C9B31DCC815A00769DF0 /* QDInterceptBackButtonEventViewController.m */, CDB8C9B41DCC815A00769DF0 /* QDLabelViewController.h */, CDB8C9B51DCC815A00769DF0 /* QDLabelViewController.m */, - CDB8C9B61DCC815A00769DF0 /* QDLinkButtonViewController.h */, - CDB8C9B71DCC815A00769DF0 /* QDLinkButtonViewController.m */, + 08ACAA6F22D66747008530C5 /* QDLargeTitlesViewController.h */, + 08ACAA7022D66747008530C5 /* QDLargeTitlesViewController.m */, + CD2FEE5A2260F6AC00298BF5 /* QDNavigationBarMaxYViewController.h */, + CD2FEE5B2260F6AC00298BF5 /* QDNavigationBarMaxYViewController.m */, CDB8C9B81DCC815A00769DF0 /* QDNavigationButtonViewController.h */, CDB8C9B91DCC815A00769DF0 /* QDNavigationButtonViewController.m */, CDB8C9BA1DCC815A00769DF0 /* QDNavigationListViewController.h */, @@ -794,20 +957,22 @@ CD4421CE1E84C74C001B8C2A /* QDObjectViewController.m */, CD4013041EFCFA300022FE2A /* QDOrientationViewController.h */, CD4013051EFCFA300022FE2A /* QDOrientationViewController.m */, + D004542D24B4A1C400F723E0 /* QDSearchBarViewController.h */, + D004542E24B4A1C400F723E0 /* QDSearchBarViewController.m */, CDB8C9BE1DCC815A00769DF0 /* QDSearchViewController.h */, CDB8C9BF1DCC815A00769DF0 /* QDSearchViewController.m */, CD5387741EE0096C00654A73 /* QDSliderViewController.h */, CD5387751EE0096C00654A73 /* QDSliderViewController.m */, - CDB8C9C21DCC815A00769DF0 /* QDTabBarItemViewController.h */, - CDB8C9C31DCC815A00769DF0 /* QDTabBarItemViewController.m */, + CDB8C9C21DCC815A00769DF0 /* QDTabBarDemoViewController.h */, + CDB8C9C31DCC815A00769DF0 /* QDTabBarDemoViewController.m */, CD6CC62B1EF7A6EA00602EDD /* QDTableViewCellAccessoryTypeViewController.h */, CD6CC62C1EF7A6EA00602EDD /* QDTableViewCellAccessoryTypeViewController.m */, CDB8C9B01DCC815A00769DF0 /* QDTableViewCellInsetsViewController.h */, CDB8C9B11DCC815A00769DF0 /* QDTableViewCellInsetsViewController.m */, - CDB8C9C41DCC815A00769DF0 /* QDTableViewCellViewController.h */, - CDB8C9C51DCC815A00769DF0 /* QDTableViewCellViewController.m */, - CDA05DA31FB18FC900606756 /* QDTableViewController.h */, - CDA05DA41FB18FC900606756 /* QDTableViewController.m */, + CDC73B432942344F00F1D584 /* QDTableViewCellReorderStyleViewController.h */, + CDC73B442942344F00F1D584 /* QDTableViewCellReorderStyleViewController.m */, + D03102B724AB1DE30095C232 /* QDTableViewCellSeparatorInsetsViewController.h */, + D03102B824AB1DE30095C232 /* QDTableViewCellSeparatorInsetsViewController.m */, CDA05DA61FB1913400606756 /* QDTableViewHeaderFooterViewController.h */, CDA05DA71FB1913400606756 /* QDTableViewHeaderFooterViewController.m */, CDB8C9C61DCC815A00769DF0 /* QDTextFieldViewController.h */, @@ -824,12 +989,6 @@ CD3CDD511F39F077008529DA /* QDUIViewDebugViewController.m */, CD3CDD531F3AAD42008529DA /* QDUIViewLayoutViewController.h */, CD3CDD541F3AAD42008529DA /* QDUIViewLayoutViewController.m */, - CDB8C9CE1DCC815A00769DF0 /* QDUIViewQMUIViewController.h */, - CDB8C9CF1DCC815A00769DF0 /* QDUIViewQMUIViewController.m */, - CD2FEE5A2260F6AC00298BF5 /* QDNavigationBarMaxYViewController.h */, - CD2FEE5B2260F6AC00298BF5 /* QDNavigationBarMaxYViewController.m */, - CD4EA4B52275D5C200A55066 /* QDImageViewViewController.h */, - CD4EA4B62275D5C200A55066 /* QDImageViewViewController.m */, ); path = UIKit; sourceTree = ""; @@ -839,8 +998,6 @@ children = ( CDD4D1EC1DCE09FE00417427 /* QDAboutViewController.h */, CDD4D1ED1DCE09FE00417427 /* QDAboutViewController.m */, - CD18CF2120F76A3D00EED53C /* QDAAViewController.h */, - CD18CF2220F76A3D00EED53C /* QDAAViewController.m */, ); path = About; sourceTree = ""; @@ -848,6 +1005,8 @@ CDE50AD11DAA6E6F000D5414 /* Frameworks */ = { isa = PBXGroup; children = ( + 8F732791262757E20051952C /* LookinServer.xcframework */, + CD4EA5A9228EDBFF00A55066 /* XCTest.framework */, CD7593641EF7A391004B5819 /* MediaPlayer.framework */, CD7593611EF7A362004B5819 /* Photos.framework */, CD7593601EF7A35B004B5819 /* libxml2.tbd */, @@ -861,6 +1020,70 @@ name = Frameworks; sourceTree = ""; }; + CDFD573A26DE132700603D1E /* QMUIKeyboard */ = { + isa = PBXGroup; + children = ( + CDFD574D26DE1BA100603D1E /* Common */, + CDFD573B26DE132800603D1E /* KeyboardViewController.h */, + CDFD573C26DE132800603D1E /* KeyboardViewController.m */, + CDFD573E26DE132800603D1E /* Info.plist */, + ); + path = QMUIKeyboard; + sourceTree = ""; + }; + CDFD574D26DE1BA100603D1E /* Common */ = { + isa = PBXGroup; + children = ( + CDFD574E26DE1BA100603D1E /* UIImage+QMUI.m */, + CDFD574F26DE1BA100603D1E /* NSMethodSignature+QMUI.h */, + CDFD575026DE1BA100603D1E /* Runtime.h */, + CDFD575126DE1BA100603D1E /* UIImage+QMUI.h */, + CDFD575226DE1BA100603D1E /* Common.h */, + CDFD575326DE1BA100603D1E /* NSMethodSignature+QMUI.m */, + ); + path = Common; + sourceTree = ""; + }; + D00809BE24C5C48C00C50AA1 /* QMUINavigationBottomAccessoryView */ = { + isa = PBXGroup; + children = ( + D00809C024C5C4AC00C50AA1 /* UINavigationItem+QMUIBottomAccessoryView.h */, + D00809C124C5C4AC00C50AA1 /* UINavigationItem+QMUIBottomAccessoryView.m */, + ); + path = QMUINavigationBottomAccessoryView; + sourceTree = ""; + }; + D00809C524C5E5A900C50AA1 /* QMUINavigationBarSmoothEffect */ = { + isa = PBXGroup; + children = ( + D00809C724C5E5A900C50AA1 /* UINavigationBar+QMUISmoothEffect.h */, + D00809C624C5E5A900C50AA1 /* UINavigationBar+QMUISmoothEffect.m */, + ); + path = QMUINavigationBarSmoothEffect; + sourceTree = ""; + }; + D03278312475365E00DF8FF3 /* QMUIInteractiveDebugger */ = { + isa = PBXGroup; + children = ( + D032783724754F9300DF8FF3 /* QMUIInteractiveDebugger.h */, + D032783524754F9300DF8FF3 /* QMUIInteractiveDebugger.m */, + D032783824754F9300DF8FF3 /* QMUIInteractiveDebugPanelViewController.h */, + D032783624754F9300DF8FF3 /* QMUIInteractiveDebugPanelViewController.m */, + D03278322475367200DF8FF3 /* QMUIInteractiveDebugPanelItem.h */, + D03278332475367200DF8FF3 /* QMUIInteractiveDebugPanelItem.m */, + ); + path = QMUIInteractiveDebugger; + sourceTree = ""; + }; + D043E67625783BA300D1E507 /* QMUIBackBarButton */ = { + isa = PBXGroup; + children = ( + D043E67725783BA300D1E507 /* QMUIBackBarButton.h */, + D043E67825783BA300D1E507 /* QMUIBackBarButton.m */, + ); + path = QMUIBackBarButton; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -872,18 +1095,54 @@ 165F42ED1ADB60180057EF6A /* Frameworks */, 165F42EE1ADB60180057EF6A /* Resources */, CDE85CB91F95E21500E622E8 /* Embed Frameworks */, - CDD4942D1E5E8C3900829B7D /* Integrate RevealServer */, + CDFD574226DE132800603D1E /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( CDE85CB81F95E21500E622E8 /* PBXTargetDependency */, + CDFD574026DE132800603D1E /* PBXTargetDependency */, ); name = qmuidemo; productName = qmuidemo; productReference = 165F42F01ADB60180057EF6A /* qmuidemo.app */; productType = "com.apple.product-type.application"; }; + CD4EA594228D87B000A55066 /* QMUIDemoUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = CD4EA5A2228D87B000A55066 /* Build configuration list for PBXNativeTarget "QMUIDemoUITests" */; + buildPhases = ( + CD4EA591228D87B000A55066 /* Sources */, + CD4EA592228D87B000A55066 /* Frameworks */, + CD4EA593228D87B000A55066 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + CD4EA59B228D87B000A55066 /* PBXTargetDependency */, + ); + name = QMUIDemoUITests; + productName = QMUIDemoUITests; + productReference = CD4EA595228D87B000A55066 /* QMUIDemoUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + CDFD573826DE132700603D1E /* QMUIKeyboard */ = { + isa = PBXNativeTarget; + buildConfigurationList = CDFD574726DE132800603D1E /* Build configuration list for PBXNativeTarget "QMUIKeyboard" */; + buildPhases = ( + CDFD573526DE132700603D1E /* Sources */, + CDFD573626DE132700603D1E /* Frameworks */, + CDFD573726DE132700603D1E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = QMUIKeyboard; + productName = QMUIKeyboard; + productReference = CDFD573926DE132700603D1E /* QMUIKeyboard.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -891,7 +1150,8 @@ isa = PBXProject; attributes = { CLASSPREFIX = QD; - LastUpgradeCheck = 0930; + DefaultBuildSystemTypeForWorkspace = Latest; + LastUpgradeCheck = 1130; ORGANIZATIONNAME = "QMUI Team"; TargetAttributes = { 165F42EF1ADB60180057EF6A = { @@ -899,14 +1159,17 @@ DevelopmentTeamName = "沛钞 陈 (Personal Team)"; ProvisioningStyle = Automatic; }; - CDE85CF01F9701D600E622E8 = { - CreatedOnToolsVersion = 9.0; - ProvisioningStyle = Automatic; + CD4EA594228D87B000A55066 = { + CreatedOnToolsVersion = 10.2.1; + TestTargetID = 165F42EF1ADB60180057EF6A; + }; + CDFD573826DE132700603D1E = { + CreatedOnToolsVersion = 12.5.1; }; }; }; buildConfigurationList = 165F42EB1ADB60180057EF6A /* Build configuration list for PBXProject "qmuidemo" */; - compatibilityVersion = "Xcode 10.0"; + compatibilityVersion = "Xcode 15.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -926,12 +1189,20 @@ projectRoot = ""; targets = ( 165F42EF1ADB60180057EF6A /* qmuidemo */, - CDE85CF01F9701D600E622E8 /* qmuidemo-rdm */, + CD4EA594228D87B000A55066 /* QMUIDemoUITests */, + CDFD573826DE132700603D1E /* QMUIKeyboard */, ); }; /* End PBXProject section */ /* Begin PBXReferenceProxy section */ + CD4EA5A1228D87B000A55066 /* QMUIKitTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = QMUIKitTests.xctest; + remoteRef = CD4EA5A0228D87B000A55066 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; CD75935B1EF7A2EC004B5819 /* QMUIKit.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; @@ -960,38 +1231,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - CDD4942D1E5E8C3900829B7D /* Integrate RevealServer */ = { - isa = PBXShellScriptBuildPhase; + CD4EA593228D87B000A55066 /* Resources */ = { + isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); - inputPaths = ( - ); - name = "Integrate RevealServer"; - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "export REVEAL_SERVER_FILENAME=\"RevealServer.framework\"\n\n# Update this path to point to the location of RevealServer.framework in your project.\nexport REVEAL_SERVER_PATH=\"${SRCROOT}/qmuidemo/Frameworks/${REVEAL_SERVER_FILENAME}\"\n\n# If configuration is not Debug, skip this script.\n[ \"${CONFIGURATION}\" == \"Release\" ] && exit 0\n\n# If RevealServer.framework exists at the specified path, run code signing script.\nif [ -d \"${REVEAL_SERVER_PATH}\" ]; then\n\"${REVEAL_SERVER_PATH}/Scripts/copy_and_codesign_revealserver.sh\"\nelse\necho \"Reveal Server not loaded: RevealServer.framework could not be found.\"\nfi"; }; - CDE85CF71F9701E300E622E8 /* RDM */ = { - isa = PBXShellScriptBuildPhase; + CDFD573726DE132700603D1E /* Resources */ = { + isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); - inputPaths = ( - ); - name = RDM; - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "[ \"${CONFIGURATION}\" != \"RDM\" ] && exit 0\n \n # compress application.\n \n /bin/mkdir -p $CONFIGURATION_BUILD_DIR/Payload\n \n /bin/cp -R $CONFIGURATION_BUILD_DIR/qmuidemo.app $CONFIGURATION_BUILD_DIR/Payload\n \n #/bin/cp images/logo_itunes.png $CONFIGURATION_BUILD_DIR/iTunesArtwork\n \n cd $CONFIGURATION_BUILD_DIR\n \n # zip up the HelloWorld directory\n \n /usr/bin/zip -r qmuidemo.ipa Payload iTunesArtwork"; }; -/* End PBXShellScriptBuildPhase section */ +/* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 165F42EC1ADB60180057EF6A /* Sources */ = { @@ -999,8 +1253,8 @@ buildActionMask = 2147483647; files = ( CDB8C9D91DCC815A00769DF0 /* QDUIHelper.m in Sources */, - CDB8CA131DCC815A00769DF0 /* QDTableViewCellViewController.m in Sources */, CDB8C9D31DCC815A00769DF0 /* QDCommonListViewController.m in Sources */, + D0AF7891257E6D2F006436A7 /* QDBackBarButtonViewController.m in Sources */, CDB8C9E41DCC815A00769DF0 /* QDMultipleImagePickerPreviewViewController.m in Sources */, CDB8C9D21DCC815A00769DF0 /* QDCommonGroupListViewController.m in Sources */, CDB8CA031DCC815A00769DF0 /* QDColorViewController.m in Sources */, @@ -1008,9 +1262,10 @@ CD4013061EFCFA300022FE2A /* QDOrientationViewController.m in Sources */, CDB8C9EB1DCC815A00769DF0 /* QDSaveImageToSpecifiedAlbumViewController.m in Sources */, CDB8C9D01DCC815A00769DF0 /* QMUIConfigurationTemplate.m in Sources */, - D021DE43205E87E700FFA408 /* QDCellKeyCacheViewController.m in Sources */, CDB8C9F91DCC815A00769DF0 /* QDAllSystemFontsViewController.m in Sources */, CD1A8AA11EC4012400B81693 /* QMUIConfigurationTemplateGrass.m in Sources */, + D05086B724CF567A00963BCE /* QDNavigationBottomAccessoryViewController.m in Sources */, + CD05C20C2750D70F0001C5E0 /* QDBlurEffectViewController.m in Sources */, CDB8C9E01DCC815A00769DF0 /* QDGridViewController.m in Sources */, CDB8C9D41DCC815A00769DF0 /* QDCommonTableViewController.m in Sources */, CD0C342521E5E2D800B781AD /* QDCellHeightCacheViewController.m in Sources */, @@ -1024,55 +1279,65 @@ CD4421D21E85071A001B8C2A /* QDObjectMethodsListViewController.m in Sources */, CDD4D1EE1DCE09FE00417427 /* QDAboutViewController.m in Sources */, CDB8C9D71DCC815A00769DF0 /* QDTabBarViewController.m in Sources */, - CD1229A71DF677AC003B9649 /* QDImagePreviewExampleViewController.m in Sources */, D02F87F81EDBE8C300EE2CA8 /* QDFontViewController.m in Sources */, CDB8C9DF1DCC815A00769DF0 /* QDEmptyViewController.m in Sources */, - CDA05DA51FB18FC900606756 /* QDTableViewController.m in Sources */, CDB8CA101DCC815A00769DF0 /* QDSearchViewController.m in Sources */, + CD0ADF6927293961002A1A54 /* QMUIDropdownNotification.m in Sources */, + CD0ADF6C2729999C002A1A54 /* QDDropdownNotificationViewController.m in Sources */, CDB8C9F71DCC815A00769DF0 /* QDReplicatorLayerViewController.m in Sources */, + CDCD270D2B8E41C400D3500A /* QDSheetPresentationViewController.m in Sources */, CDB8C9DA1DCC815A00769DF0 /* UIImageEffects.m in Sources */, CD9D6EC6211064060004E222 /* QDCAAnimationViewController.m in Sources */, + CDA9537C2814460300D0FF0E /* LookinConfig.m in Sources */, + D05086B424CF383B00963BCE /* QDNavigationBarSmoothEffectViewController.m in Sources */, CD4EA4B72275D5C200A55066 /* QDImageViewViewController.m in Sources */, + CDC73B452942344F00F1D584 /* QDTableViewCellReorderStyleViewController.m in Sources */, CD1A8AA41EC4045500B81693 /* QMUIConfigurationTemplatePinkRose.m in Sources */, + D004543224B6014B00F723E0 /* QDStyleSelectableTableViewController.m in Sources */, CD18BD8521871DCE00E2A4E6 /* QDNavigationBarScrollingSnapAnimatorViewController.m in Sources */, + D0B1856822C5016F002578CD /* QMUIConfigurationTemplateDark.m in Sources */, D021DE46205E889100FFA408 /* QDCellSizeKeyCacheViewController.m in Sources */, CDB8C9DC1DCC815A00769DF0 /* QDComponentsViewController.m in Sources */, + CD60DB552C5BDBE9005109B3 /* QDCheckboxViewController.m in Sources */, + D00809C924C5E5AA00C50AA1 /* UINavigationBar+QMUISmoothEffect.m in Sources */, CD4A05F220288980000159E9 /* QDNavigationTransitionViewController.m in Sources */, CDB8C9FE1DCC815A00769DF0 /* QDChangeNavBarStyleViewController.m in Sources */, 165F42F91ADB60180057EF6A /* AppDelegate.m in Sources */, CDB8C9EC1DCC815A00769DF0 /* QDSaveVideoToSpecifiedAlbumViewController.m in Sources */, CDB8C9FC1DCC815A00769DF0 /* QDAlertController.m in Sources */, CDB8CA0E1DCC815A00769DF0 /* QDNavigationListViewController.m in Sources */, - CD18CF2320F76A3D00EED53C /* QDAAViewController.m in Sources */, CDB8C9DB1DCC815A00769DF0 /* QDAssetsManagerViewController.m in Sources */, - CDB8C9EF1DCC815A00769DF0 /* QDAnimationViewController.m in Sources */, CDB8C9ED1DCC815A00769DF0 /* QDSingleImagePickerPreviewViewController.m in Sources */, + CD60DB4C2C5BBC65005109B3 /* QDPopupMenuViewController.m in Sources */, D0283ECC20C2F2290090DD45 /* QDBadgeViewController.m in Sources */, CDB8C9E51DCC815A00769DF0 /* QDNavigationTitleViewController.m in Sources */, + D03102B924AB1DE30095C232 /* QDTableViewCellSeparatorInsetsViewController.m in Sources */, CDB8C9FF1DCC815A00769DF0 /* QDCollectionStackDemoViewController.m in Sources */, + D032783924754F9400DF8FF3 /* QMUIInteractiveDebugger.m in Sources */, CDA05DA81FB1913400606756 /* QDTableViewHeaderFooterViewController.m in Sources */, CDB8C9E31DCC815A00769DF0 /* QDMoreOperationViewController.m in Sources */, - CDB8CA071DCC815A00769DF0 /* QDGhostButtonViewController.m in Sources */, CDA1CC951EC1BF5200AB8A0F /* QMUIConfigurationTemplateGrapefruit.m in Sources */, FE5803331E949E1100159380 /* QDKeyboardViewController.m in Sources */, + D032783A24754F9400DF8FF3 /* QMUIInteractiveDebugPanelViewController.m in Sources */, CDB8C9EE1DCC815A00769DF0 /* QDAllAnimationViewController.m in Sources */, CDB8CA0A1DCC815A00769DF0 /* QDInterceptBackButtonEventViewController.m in Sources */, + D0911AC2249B11AE00112D88 /* QDInteractiveDebugViewController.m in Sources */, CDB8CA0D1DCC815A00769DF0 /* QDNavigationButtonViewController.m in Sources */, CDB8C9FD1DCC815A00769DF0 /* QDButtonViewController.m in Sources */, CDB8C9D61DCC815A00769DF0 /* QDNavigationController.m in Sources */, CD4D339E1F010FD100FB81B0 /* QDStaticTableViewController.m in Sources */, + D004542F24B4A1C400F723E0 /* QDSearchBarViewController.m in Sources */, CDB8C9D51DCC815A00769DF0 /* QDCommonViewController.m in Sources */, CDB8C9D11DCC815A00769DF0 /* QDCommonGridViewController.m in Sources */, CD5387761EE0096C00654A73 /* QDSliderViewController.m in Sources */, CDA74F9C206BC1D000AE3830 /* QDMultipleDelegatesViewController.m in Sources */, - CDB8CA181DCC815A00769DF0 /* QDUIViewQMUIViewController.m in Sources */, CDB8C9E71DCC815A00769DF0 /* QDPopupContainerViewController.m in Sources */, CDB8CA061DCC815A00769DF0 /* QDFoldCollectionViewLayout.m in Sources */, CDB8C9DE1DCC815A00769DF0 /* QDEmotionsViewController.m in Sources */, - CDB8CA051DCC815A00769DF0 /* QDFillButtonViewController.m in Sources */, + D02A61D824FFCC5000E670D0 /* QDControlViewController.m in Sources */, 165F42F61ADB60180057EF6A /* main.m in Sources */, - CD766F7E216B6A6D005155BD /* QDScrollAnimatorViewController.m in Sources */, CDB8C9D81DCC815A00769DF0 /* QDCommonUI.m in Sources */, + 08ACAA7122D66747008530C5 /* QDLargeTitlesViewController.m in Sources */, CD1229AF1DF678FB003B9649 /* QDImagePreviewViewController1.m in Sources */, CDB8CA001DCC815A00769DF0 /* QDCollectionDemoViewController.m in Sources */, CDB8C9FA1DCC815A00769DF0 /* QDFontPointSizeAndLineHeightViewController.m in Sources */, @@ -1080,48 +1345,78 @@ CDB8CA141DCC815A00769DF0 /* QDTextFieldViewController.m in Sources */, CDB8CA171DCC815A00769DF0 /* QDUIKitViewController.m in Sources */, CDB8CA161DCC815A00769DF0 /* QDToolBarButtonViewController.m in Sources */, + CDB58B252B46238F002D4894 /* QDLayouterViewController.m in Sources */, CD0C342221E5E25200B781AD /* QDDynamicHeightTableViewCell.m in Sources */, CD6CC62D1EF7A6EA00602EDD /* QDTableViewCellAccessoryTypeViewController.m in Sources */, + D00809C224C5C4AC00C50AA1 /* UINavigationItem+QMUIBottomAccessoryView.m in Sources */, + CD7A9A1122C4BC6D0093DAB4 /* QDThemeExampleView.m in Sources */, CD6BE204205BF41500BE093E /* QDCellHeightKeyCacheViewController.m in Sources */, CDB8CA151DCC815A00769DF0 /* QDTextViewController.m in Sources */, CD3CDD551F3AAD42008529DA /* QDUIViewLayoutViewController.m in Sources */, CD3CDD4F1F39E977008529DA /* QDUIViewBorderViewController.m in Sources */, - CDB8CA0C1DCC815A00769DF0 /* QDLinkButtonViewController.m in Sources */, + D062F65B22BCD4C700737AD2 /* QDThemeViewController.m in Sources */, CDB8C9E61DCC815A00769DF0 /* QDPieProgressViewController.m in Sources */, CD9207261DD49CD100AE32C0 /* QDFloatLayoutViewController.m in Sources */, CDB8CA0F1DCC815A00769DF0 /* QDNormalButtonViewController.m in Sources */, - CDB8CA011DCC815A00769DF0 /* QDCollectionListViewController.m in Sources */, + D043E67A25783BA300D1E507 /* QMUIBackBarButton.m in Sources */, + D03278342475367200DF8FF3 /* QMUIInteractiveDebugPanelItem.m in Sources */, CDF79AAF1F16531100EE8DE9 /* QDButtonEdgeInsetsViewController.m in Sources */, CDB8CA081DCC815A00769DF0 /* QDImageViewController.m in Sources */, 1679E32E1DFFC0AD0072B8A1 /* QDCustomToastAnimator.m in Sources */, CDB8CA021DCC815A00769DF0 /* QDCollectionViewDemoCell.m in Sources */, - CDB8CA121DCC815A00769DF0 /* QDTabBarItemViewController.m in Sources */, + CDB8CA121DCC815A00769DF0 /* QDTabBarDemoViewController.m in Sources */, CD4421CF1E84C74C001B8C2A /* QDObjectViewController.m in Sources */, D052631521EE080E00D4F57A /* QDConsoleViewController.m in Sources */, + D0BEFAA52484FEEC0006D1B9 /* QDInsetGroupedTableViewController.m in Sources */, CDB8C9FB1DCC815A00769DF0 /* QDLabViewController.m in Sources */, CD18BD8221870EFD00E2A4E6 /* QDNavigationBarScrollingAnimatorViewController.m in Sources */, - CD1A8A721EC3110300B81693 /* QDThemeViewController.m in Sources */, CDB8C9F01DCC815A00769DF0 /* QDCAShapeLoadingViewController.m in Sources */, CDB8C9F81DCC815A00769DF0 /* QDRippleAnimationViewController.m in Sources */, CD3CDD521F39F077008529DA /* QDUIViewDebugViewController.m in Sources */, CDB8CA0B1DCC815A00769DF0 /* QDLabelViewController.m in Sources */, CD2FEE5C2260F6AC00298BF5 /* QDNavigationBarMaxYViewController.m in Sources */, + CD05B5E22743E1240001C5E0 /* QDAnimationCurvesViewController.m in Sources */, CDB8C9DD1DCC815A00769DF0 /* QDDialogViewController.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + CD4EA591228D87B000A55066 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CD4EA5A8228E9FCE00A55066 /* QDUITestTools.m in Sources */, + CD4EA598228D87B000A55066 /* QMUIDemoUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CDFD573526DE132700603D1E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CDFD575526DE1BA100603D1E /* NSMethodSignature+QMUI.m in Sources */, + CDFD573D26DE132800603D1E /* KeyboardViewController.m in Sources */, + CDFD575426DE1BA100603D1E /* UIImage+QMUI.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + CD4EA59B228D87B000A55066 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 165F42EF1ADB60180057EF6A /* qmuidemo */; + targetProxy = CD4EA59A228D87B000A55066 /* PBXContainerItemProxy */; + }; CDE85CB81F95E21500E622E8 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = QMUIKit; targetProxy = CDE85CB71F95E21500E622E8 /* PBXContainerItemProxy */; }; - CDE85CF61F9701E000E622E8 /* PBXTargetDependency */ = { + CDFD574026DE132800603D1E /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 165F42EF1ADB60180057EF6A /* qmuidemo */; - targetProxy = CDE85CF51F9701E000E622E8 /* PBXContainerItemProxy */; + platformFilter = ios; + target = CDFD573826DE132700603D1E /* QMUIKeyboard */; + targetProxy = CDFD573F26DE132800603D1E /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -1208,7 +1503,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -1257,7 +1552,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1273,11 +1568,12 @@ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = NO; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CODE_SIGN_ENTITLEMENTS = ""; + CODE_SIGN_ENTITLEMENTS = qmuidemo/qmuidemo.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = FP989NU38H; + CURRENT_PROJECT_VERSION = 17; + DEVELOPMENT_TEAM = PM6KH29YH2; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -1286,24 +1582,30 @@ ); GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = ./qmuidemo/PrefixHeader.pch; - HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/QMUI/**"; + HEADER_SEARCH_PATHS = ""; INFOPLIST_FILE = qmuidemo/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 4.8.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-weak_framework", - RevealServer, + LookinServer, ); - PRODUCT_BUNDLE_IDENTIFIER = "com.qmuidemo.qmui-debug"; + "OTHER_LDFLAGS[sdk=macosx*]" = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.molice.test; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; WARNING_CFLAGS = "-Wno-nullability-completeness"; }; name = Debug; @@ -1316,10 +1618,12 @@ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = NO; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CODE_SIGN_ENTITLEMENTS = qmuidemo/qmuidemo.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = FP989NU38H; + CURRENT_PROJECT_VERSION = 17; + DEVELOPMENT_TEAM = PM6KH29YH2; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -1328,142 +1632,156 @@ ); GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = ./qmuidemo/PrefixHeader.pch; - HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/QMUI/**"; + HEADER_SEARCH_PATHS = ""; INFOPLIST_FILE = qmuidemo/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 4.8.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", ); - PRODUCT_BUNDLE_IDENTIFIER = com.qmuidemo.qmui; + PRODUCT_BUNDLE_IDENTIFIER = com.molice.test; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + TARGETED_DEVICE_FAMILY = "1,2"; WARNING_CFLAGS = "-Wno-nullability-completeness"; }; name = Release; }; - CDE85C9E1F95D70F00E622E8 /* RDM */ = { + CD4EA59C228D87B000A55066 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = QMUIDemoUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.qmui.QMUIDemoUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; + TEST_TARGET_NAME = qmuidemo; }; - name = RDM; + name = Debug; }; - CDE85C9F1F95D70F00E622E8 /* RDM */ = { + CD4EA59D228D87B000A55066 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - BUNDLE_DISPLAY_NAME = "QMUI (RDM)"; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = NO; - CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CODE_SIGN_IDENTITY = "iPhone Distribution: Tencent Technology (Shenzhen) Co., Ltd"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: Tencent Technology (Shenzhen) Co., Ltd"; - CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = HKB8WPFBR9; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)", - "$(PROJECT_DIR)/qmuidemo/Frameworks", - ); - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = ./qmuidemo/PrefixHeader.pch; - GCC_PREPROCESSOR_DEFINITIONS = ""; - HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/QMUI/**"; - INFOPLIST_FILE = qmuidemo/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = QMUIDemoUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", + "@loader_path/Frameworks", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-ObjC", - "-weak_framework", - RevealServer, - ); - PRODUCT_BUNDLE_IDENTIFIER = com.qmuidemo.qmui; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.qmui.QMUIDemoUITests; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = ""; - PROVISIONING_PROFILE_SPECIFIER = "ea2ce615-e8b3-4c97-87a1-b8386d67eb8c"; - SDKROOT = iphoneos; - WARNING_CFLAGS = "-Wno-nullability-completeness"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = qmuidemo; }; - name = RDM; + name = Release; }; - CDE85CF21F9701D600E622E8 /* Debug */ = { + CDFD574326DE132800603D1E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 17; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = PM6KH29YH2; + ENABLE_BITCODE = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = QMUIKeyboard/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 4.8.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.molice.test.keyboard; PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - CDE85CF31F9701D600E622E8 /* Release */ = { + CDFD574426DE132800603D1E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 17; + DEVELOPMENT_TEAM = PM6KH29YH2; + ENABLE_BITCODE = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = QMUIKeyboard/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 4.8.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.molice.test.keyboard; PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; - CDE85CF41F9701D600E622E8 /* RDM */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = RDM; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1472,30 +1790,36 @@ buildConfigurations = ( 165F43111ADB60180057EF6A /* Debug */, 165F43121ADB60180057EF6A /* Release */, - CDE85C9E1F95D70F00E622E8 /* RDM */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = RDM; + defaultConfigurationName = Debug; }; 165F43131ADB60180057EF6A /* Build configuration list for PBXNativeTarget "qmuidemo" */ = { isa = XCConfigurationList; buildConfigurations = ( 165F43141ADB60180057EF6A /* Debug */, 165F43151ADB60180057EF6A /* Release */, - CDE85C9F1F95D70F00E622E8 /* RDM */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = RDM; + defaultConfigurationName = Debug; + }; + CD4EA5A2228D87B000A55066 /* Build configuration list for PBXNativeTarget "QMUIDemoUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CD4EA59C228D87B000A55066 /* Debug */, + CD4EA59D228D87B000A55066 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; }; - CDE85CF11F9701D600E622E8 /* Build configuration list for PBXAggregateTarget "qmuidemo-rdm" */ = { + CDFD574726DE132800603D1E /* Build configuration list for PBXNativeTarget "QMUIKeyboard" */ = { isa = XCConfigurationList; buildConfigurations = ( - CDE85CF21F9701D600E622E8 /* Debug */, - CDE85CF31F9701D600E622E8 /* Release */, - CDE85CF41F9701D600E622E8 /* RDM */, + CDFD574326DE132800603D1E /* Debug */, + CDFD574426DE132800603D1E /* Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = RDM; + defaultConfigurationName = Debug; }; /* End XCConfigurationList section */ }; diff --git a/qmuidemo.xcodeproj/xcshareddata/xcschemes/qmuidemo.xcscheme b/qmuidemo.xcodeproj/xcshareddata/xcschemes/qmuidemo.xcscheme index 29a00c5e..681f95ed 100644 --- a/qmuidemo.xcodeproj/xcshareddata/xcschemes/qmuidemo.xcscheme +++ b/qmuidemo.xcodeproj/xcshareddata/xcschemes/qmuidemo.xcscheme @@ -1,6 +1,6 @@ - - - - + + + + + + - - -@interface AppDelegate : UIResponder +@interface AppDelegate : UIResponder @property(strong, nonatomic) UIWindow *window; diff --git a/qmuidemo/AppDelegate.m b/qmuidemo/AppDelegate.m index 8aa09686..aac6aeaa 100644 --- a/qmuidemo/AppDelegate.m +++ b/qmuidemo/AppDelegate.m @@ -14,11 +14,68 @@ #import "QDUIKitViewController.h" #import "QDComponentsViewController.h" #import "QDLabViewController.h" +#import "QMUIConfigurationTemplateGrapefruit.h" +#import "QMUIConfigurationTemplateGrass.h" +#import "QMUIConfigurationTemplatePinkRose.h" +#import "QMUIConfigurationTemplateDark.h" + +//#define UIWindowScene_Enabled @implementation AppDelegate +#ifdef UIWindowScene_Enabled + +#pragma mark - + +- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { + if ([scene isKindOfClass:UIWindowScene.class]) { + self.window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene]; + [self didInitWindow]; + } +} + +#endif + +#pragma mark - + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // 1. 先注册主题监听,在回调里将主题持久化存储,避免启动过程中主题发生变化时读取到错误的值 + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleThemeDidChangeNotification:) name:QMUIThemeDidChangeNotification object:nil]; + + // 2. 然后设置主题的生成器 + QMUIThemeManagerCenter.defaultThemeManager.themeGenerator = ^__kindof NSObject * _Nonnull(NSString * _Nonnull identifier) { + if ([identifier isEqualToString:QDThemeIdentifierDefault]) return QMUIConfigurationTemplate.new; + if ([identifier isEqualToString:QDThemeIdentifierGrapefruit]) return QMUIConfigurationTemplateGrapefruit.new; + if ([identifier isEqualToString:QDThemeIdentifierGrass]) return QMUIConfigurationTemplateGrass.new; + if ([identifier isEqualToString:QDThemeIdentifierPinkRose]) return QMUIConfigurationTemplatePinkRose.new; + if ([identifier isEqualToString:QDThemeIdentifierDark]) return QMUIConfigurationTemplateDark.new; + return nil; + }; + + // 3. 再针对 iOS 13 开启自动响应系统的 Dark Mode 切换 + // 如果不需要这个功能,则不需要这一段代码 + if (QMUIThemeManagerCenter.defaultThemeManager.currentThemeIdentifier) {// 做这个 if(currentThemeIdentifier) 的保护只是为了避免 QD 里的配置表没启动时,没人为 currentTheme/currentThemeIdentifier 赋值,导致后续的逻辑会 crash,业务项目里理论上不会有这种情况出现,所以可以省略这个 if,直接写下面的代码就行了 + + QMUIThemeManagerCenter.defaultThemeManager.identifierForTrait = ^__kindof NSObject * _Nonnull(UITraitCollection * _Nonnull trait) { + // 1. 如果当前系统切换到 Dark Mode,则返回 App 在 Dark Mode 下的主题 + if (trait.userInterfaceStyle == UIUserInterfaceStyleDark) { + return QDThemeIdentifierDark; + } + + // 2. 如果没有命中1,说明此时系统是 Light,则返回 App 在 Light 下的主题即可,这里不直接返回 Default,而是先做一些复杂判断,是因为 QMUI Demo 非深色模式的主题有好几个,而我们希望不管之前选择的是 Default、Grapefruit 还是 PinkRose,只要从 Dark 切换为非 Dark,都强制改为 Default。 + + // 换句话说,如果业务项目只有 Light/Dark 两套主题,则按下方被注释掉的代码一样直接返回 Light 下的主题即可。 +// return QDThemeIdentifierDefault; + + if ([QMUIThemeManagerCenter.defaultThemeManager.currentThemeIdentifier isEqual:QDThemeIdentifierDark]) { + return QDThemeIdentifierDefault; + } + return QMUIThemeManagerCenter.defaultThemeManager.currentThemeIdentifier; + }; + QMUIThemeManagerCenter.defaultThemeManager.respondsSystemStyleAutomatically = YES; + } + // QMUIConsole 默认只在 DEBUG 下会显示,作为 Demo,改为不管什么环境都允许显示 [QMUIConsole sharedInstance].canShow = YES; @@ -26,52 +83,56 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [QDCommonUI renderGlobalAppearances]; // 预加载 QQ 表情,避免第一次使用时卡顿 - dispatch_async(dispatch_get_global_queue(0, 0), ^{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [QDUIHelper qmuiEmotions]; }); +#ifndef UIWindowScene_Enabled // 界面 - self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; - [self createTabBarController]; - - // 启动动画 - [self startLaunchingAnimation]; + self.window = [[UIWindow alloc] init]; + [self didInitWindow]; +#endif return YES; } -- (void)createTabBarController { +- (void)didInitWindow { + self.window.rootViewController = [self generateWindowRootViewController]; + [self.window makeKeyAndVisible]; + [self startLaunchingAnimation]; +} + +- (UIViewController *)generateWindowRootViewController { QDTabBarViewController *tabBarViewController = [[QDTabBarViewController alloc] init]; // QMUIKit QDUIKitViewController *uikitViewController = [[QDUIKitViewController alloc] init]; uikitViewController.hidesBottomBarWhenPushed = NO; QDNavigationController *uikitNavController = [[QDNavigationController alloc] initWithRootViewController:uikitViewController]; - uikitNavController.tabBarItem = [QDUIHelper tabBarItemWithTitle:@"QMUIKit" image:[UIImageMake(@"icon_tabbar_uikit") imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] selectedImage:UIImageMake(@"icon_tabbar_uikit_selected") tag:0]; + uikitNavController.tabBarItem = [QDUIHelper tabBarItemWithTitle:@"QMUIKit" image:UIImageMake(@"icon_tabbar_uikit") selectedImage:UIImageMake(@"icon_tabbar_uikit_selected") tag:0]; AddAccessibilityHint(uikitNavController.tabBarItem, @"展示一系列对系统原生控件的拓展的能力"); // UIComponents QDComponentsViewController *componentViewController = [[QDComponentsViewController alloc] init]; componentViewController.hidesBottomBarWhenPushed = NO; QDNavigationController *componentNavController = [[QDNavigationController alloc] initWithRootViewController:componentViewController]; - componentNavController.tabBarItem = [QDUIHelper tabBarItemWithTitle:@"Components" image:[UIImageMake(@"icon_tabbar_component") imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] selectedImage:UIImageMake(@"icon_tabbar_component_selected") tag:1]; + componentNavController.tabBarItem = [QDUIHelper tabBarItemWithTitle:@"Components" image:UIImageMake(@"icon_tabbar_component") selectedImage:UIImageMake(@"icon_tabbar_component_selected") tag:1]; AddAccessibilityHint(componentNavController.tabBarItem, @"展示 QMUI 自己的组件库"); // Lab QDLabViewController *labViewController = [[QDLabViewController alloc] init]; labViewController.hidesBottomBarWhenPushed = NO; QDNavigationController *labNavController = [[QDNavigationController alloc] initWithRootViewController:labViewController]; - labNavController.tabBarItem = [QDUIHelper tabBarItemWithTitle:@"Lab" image:[UIImageMake(@"icon_tabbar_lab") imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] selectedImage:UIImageMake(@"icon_tabbar_lab_selected") tag:2]; + labNavController.tabBarItem = [QDUIHelper tabBarItemWithTitle:@"Lab" image:UIImageMake(@"icon_tabbar_lab") selectedImage:UIImageMake(@"icon_tabbar_lab_selected") tag:2]; AddAccessibilityHint(labNavController.tabBarItem, @"集合一些非正式但可能很有用的小功能"); // window root controller tabBarViewController.viewControllers = @[uikitNavController, componentNavController, labNavController]; - self.window.rootViewController = tabBarViewController; - [self.window makeKeyAndVisible]; + return tabBarViewController; } - (void)startLaunchingAnimation { - UIWindow *window = [[[UIApplication sharedApplication] delegate] window]; + UIWindow *window = self.window; UIView *launchScreenView = [[NSBundle mainBundle] loadNibNamed:@"LaunchScreen" owner:self options:nil].firstObject; launchScreenView.frame = window.bounds; [window addSubview:launchScreenView]; @@ -100,6 +161,7 @@ - (void)startLaunchingAnimation { [UIView animateWithDuration:.15 delay:0.9 options:QMUIViewAnimationOptionsCurveOut animations:^{ [launchScreenView layoutIfNeeded]; logoImageView.alpha = 0.0; + logoImageView.transform = CGAffineTransformMakeScale(3, 3); copyrightLabel.alpha = 0; } completion:nil]; [UIView animateWithDuration:1.2 delay:0.9 options:UIViewAnimationOptionCurveEaseOut animations:^{ @@ -110,4 +172,22 @@ - (void)startLaunchingAnimation { }]; } +- (void)handleThemeDidChangeNotification:(NSNotification *)notification { + + QMUIThemeManager *manager = notification.object; + if (![manager.name isEqual:QMUIThemeManagerNameDefault]) return; + + [[NSUserDefaults standardUserDefaults] setObject:manager.currentThemeIdentifier forKey:QDSelectedThemeIdentifier]; + + [QDThemeManager.currentTheme applyConfigurationTemplate]; + + if (QMUIHelper.canUpdateAppearance) { + // 主题发生变化,在这里更新全局 UI 控件的 appearance + [QDCommonUI renderGlobalAppearances]; + + // 更新表情 icon 的颜色 + [QDUIHelper updateEmotionImages]; + } +} + @end diff --git a/qmuidemo/Base.lproj/LaunchScreen.xib b/qmuidemo/Base.lproj/LaunchScreen.xib index 560674fe..5a307765 100644 --- a/qmuidemo/Base.lproj/LaunchScreen.xib +++ b/qmuidemo/Base.lproj/LaunchScreen.xib @@ -1,11 +1,9 @@ - - - - + + - + @@ -15,14 +13,18 @@ - + + + + + - - diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/Info.plist b/qmuidemo/Frameworks/LookinServer.xcframework/Info.plist new file mode 100644 index 00000000..f497eca5 --- /dev/null +++ b/qmuidemo/Frameworks/LookinServer.xcframework/Info.plist @@ -0,0 +1,41 @@ + + + + + AvailableLibraries + + + LibraryIdentifier + ios-arm64_armv7 + LibraryPath + LookinServer.framework + SupportedArchitectures + + arm64 + armv7 + + SupportedPlatform + ios + + + LibraryIdentifier + ios-arm64_x86_64-simulator + LibraryPath + LookinServer.framework + SupportedArchitectures + + arm64 + x86_64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/Info.plist b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/Info.plist new file mode 100644 index 00000000..cc96cfb4 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/Info.plist differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServer b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServer new file mode 100755 index 00000000..9ef40016 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServer differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_button@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_button@2x.png new file mode 100644 index 00000000..bd42a7be Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_button@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_button@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_button@3x.png new file mode 100644 index 00000000..2498798c Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_button@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_button_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_button_selected@2x.png new file mode 100644 index 00000000..8aa337d1 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_button_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_button_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_button_selected@3x.png new file mode 100644 index 00000000..db9313bd Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_button_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent@2x.png new file mode 100644 index 00000000..c0d26a20 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent@3x.png new file mode 100644 index 00000000..462950e8 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent_selected@2x.png new file mode 100644 index 00000000..1a835626 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent_selected@3x.png new file mode 100644 index 00000000..49be15c0 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell@2x.png new file mode 100644 index 00000000..a657b3ad Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell@3x.png new file mode 100644 index 00000000..85cbaf6c Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell_selected@2x.png new file mode 100644 index 00000000..dcb28f9e Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell_selected@3x.png new file mode 100644 index 00000000..8d1c4e06 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview@2x.png new file mode 100644 index 00000000..6656e929 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview@3x.png new file mode 100644 index 00000000..661ed8eb Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview_selected@2x.png new file mode 100644 index 00000000..91705505 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview_selected@3x.png new file mode 100644 index 00000000..6db3895c Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview@2x.png new file mode 100644 index 00000000..096f01e9 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview@3x.png new file mode 100644 index 00000000..5e4496d3 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview_selected@2x.png new file mode 100644 index 00000000..ea79c0dd Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview_selected@3x.png new file mode 100644 index 00000000..9104c877 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_control@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_control@2x.png new file mode 100644 index 00000000..bfb79da1 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_control@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_control@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_control@3x.png new file mode 100644 index 00000000..72a681a2 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_control@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_control_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_control_selected@2x.png new file mode 100644 index 00000000..7dce2658 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_control_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_control_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_control_selected@3x.png new file mode 100644 index 00000000..ef52944b Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_control_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_controller@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_controller@2x.png new file mode 100644 index 00000000..43fec0e9 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_controller@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_controller@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_controller@3x.png new file mode 100644 index 00000000..3771c641 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_controller@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer@2x.png new file mode 100644 index 00000000..0fd725d5 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer@3x.png new file mode 100644 index 00000000..cf10e3ab Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer_selected@2x.png new file mode 100644 index 00000000..3f8452ac Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer_selected@3x.png new file mode 100644 index 00000000..314108c3 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview@2x.png new file mode 100644 index 00000000..6e444c2b Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview@3x.png new file mode 100644 index 00000000..ce1b7d4a Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview_selected@2x.png new file mode 100644 index 00000000..40d76dda Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview_selected@3x.png new file mode 100644 index 00000000..0c81c0ef Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_label@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_label@2x.png new file mode 100644 index 00000000..203afaed Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_label@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_label@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_label@3x.png new file mode 100644 index 00000000..589a70cc Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_label@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_label_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_label_selected@2x.png new file mode 100644 index 00000000..b6928962 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_label_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_label_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_label_selected@3x.png new file mode 100644 index 00000000..1142e012 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_label_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer@2x.png new file mode 100644 index 00000000..779bb8dc Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer@3x.png new file mode 100644 index 00000000..64bba6b8 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer_selected@2x.png new file mode 100644 index 00000000..e8ab24f9 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer_selected@3x.png new file mode 100644 index 00000000..b183adb3 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar@2x.png new file mode 100644 index 00000000..16d29406 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar@3x.png new file mode 100644 index 00000000..681cc225 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar_selected@2x.png new file mode 100644 index 00000000..38324651 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar_selected@3x.png new file mode 100644 index 00000000..d99be236 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview@2x.png new file mode 100644 index 00000000..e38b03ba Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview@3x.png new file mode 100644 index 00000000..145c7333 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview_selected@2x.png new file mode 100644 index 00000000..f79c421f Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview_selected@3x.png new file mode 100644 index 00000000..bc0552d2 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer@2x.png new file mode 100644 index 00000000..e5ded762 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer@3x.png new file mode 100644 index 00000000..0e306225 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer_selected@2x.png new file mode 100644 index 00000000..764295ab Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer_selected@3x.png new file mode 100644 index 00000000..b72c53d7 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider@2x.png new file mode 100644 index 00000000..16d1d6a4 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider@3x.png new file mode 100644 index 00000000..5acccf11 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider_selected@2x.png new file mode 100644 index 00000000..12ab7739 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider_selected@3x.png new file mode 100644 index 00000000..1c97033f Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar@2x.png new file mode 100644 index 00000000..dcb1821f Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar@3x.png new file mode 100644 index 00000000..7842b256 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar_selected@2x.png new file mode 100644 index 00000000..e3e2b4fb Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar_selected@3x.png new file mode 100644 index 00000000..2c7f755d Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell@2x.png new file mode 100644 index 00000000..a52448d0 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell@3x.png new file mode 100644 index 00000000..6bcbc34b Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell_selected@2x.png new file mode 100644 index 00000000..0cc8db83 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell_selected@3x.png new file mode 100644 index 00000000..7416b3e6 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator@2x.png new file mode 100644 index 00000000..becb3fec Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator@3x.png new file mode 100644 index 00000000..f38f6f96 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator_selected@2x.png new file mode 100644 index 00000000..2453c45c Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator_selected@3x.png new file mode 100644 index 00000000..58a1f4b3 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter@2x.png new file mode 100644 index 00000000..6846d742 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter@3x.png new file mode 100644 index 00000000..33492eef Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@2x.png new file mode 100644 index 00000000..2ae4228a Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@3x.png new file mode 100644 index 00000000..935fd6da Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview@2x.png new file mode 100644 index 00000000..5e20879d Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview@3x.png new file mode 100644 index 00000000..a787b2d4 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview_selected@2x.png new file mode 100644 index 00000000..0ea1e3a5 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview_selected@3x.png new file mode 100644 index 00000000..41d11cf9 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield@2x.png new file mode 100644 index 00000000..48f6638c Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield@3x.png new file mode 100644 index 00000000..b157d034 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield_selected@2x.png new file mode 100644 index 00000000..9a2dc621 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield_selected@3x.png new file mode 100644 index 00000000..5aa2d9ad Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview@2x.png new file mode 100644 index 00000000..66a51fb3 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview@3x.png new file mode 100644 index 00000000..be147386 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview_selected@2x.png new file mode 100644 index 00000000..bc5bf2a1 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview_selected@3x.png new file mode 100644 index 00000000..be3e6719 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_view@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_view@2x.png new file mode 100644 index 00000000..43e0b50a Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_view@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_view@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_view@3x.png new file mode 100644 index 00000000..b1375397 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_view@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_view_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_view_selected@2x.png new file mode 100644 index 00000000..392f48ad Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_view_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_view_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_view_selected@3x.png new file mode 100644 index 00000000..5550103f Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_view_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview@2x.png new file mode 100644 index 00000000..57cb226c Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview@3x.png new file mode 100644 index 00000000..e5f883de Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview_selected@2x.png new file mode 100644 index 00000000..f7b2926f Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview_selected@3x.png new file mode 100644 index 00000000..b772fab6 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_window@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_window@2x.png new file mode 100644 index 00000000..698cf86c Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_window@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_window@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_window@3x.png new file mode 100644 index 00000000..67390d25 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/LookinServerImages.bundle/hierarchy_window@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/Modules/module.modulemap b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/Modules/module.modulemap new file mode 100644 index 00000000..36ab6fbb --- /dev/null +++ b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module LookinServer { + umbrella header "LookinServer.h" + + export * + module * { export * } +} diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/en.lproj/Localizable.strings b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/en.lproj/Localizable.strings new file mode 100644 index 00000000..53b6f85e Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/en.lproj/Localizable.strings differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/zh-Hans.lproj/Localizable.strings b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/zh-Hans.lproj/Localizable.strings new file mode 100644 index 00000000..9f2320b4 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_armv7/LookinServer.framework/zh-Hans.lproj/Localizable.strings differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/Info.plist b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/Info.plist new file mode 100644 index 00000000..34fbd917 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/Info.plist differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServer b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServer new file mode 100755 index 00000000..9cef2bb9 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServer differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_button@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_button@2x.png new file mode 100644 index 00000000..bd42a7be Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_button@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_button@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_button@3x.png new file mode 100644 index 00000000..2498798c Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_button@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_button_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_button_selected@2x.png new file mode 100644 index 00000000..8aa337d1 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_button_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_button_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_button_selected@3x.png new file mode 100644 index 00000000..db9313bd Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_button_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent@2x.png new file mode 100644 index 00000000..c0d26a20 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent@3x.png new file mode 100644 index 00000000..462950e8 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent_selected@2x.png new file mode 100644 index 00000000..1a835626 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent_selected@3x.png new file mode 100644 index 00000000..49be15c0 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_cellcontent_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell@2x.png new file mode 100644 index 00000000..a657b3ad Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell@3x.png new file mode 100644 index 00000000..85cbaf6c Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell_selected@2x.png new file mode 100644 index 00000000..dcb28f9e Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell_selected@3x.png new file mode 100644 index 00000000..8d1c4e06 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectioncell_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview@2x.png new file mode 100644 index 00000000..6656e929 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview@3x.png new file mode 100644 index 00000000..661ed8eb Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview_selected@2x.png new file mode 100644 index 00000000..91705505 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview_selected@3x.png new file mode 100644 index 00000000..6db3895c Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionreuseview_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview@2x.png new file mode 100644 index 00000000..096f01e9 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview@3x.png new file mode 100644 index 00000000..5e4496d3 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview_selected@2x.png new file mode 100644 index 00000000..ea79c0dd Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview_selected@3x.png new file mode 100644 index 00000000..9104c877 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_collectionview_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_control@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_control@2x.png new file mode 100644 index 00000000..bfb79da1 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_control@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_control@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_control@3x.png new file mode 100644 index 00000000..72a681a2 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_control@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_control_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_control_selected@2x.png new file mode 100644 index 00000000..7dce2658 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_control_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_control_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_control_selected@3x.png new file mode 100644 index 00000000..ef52944b Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_control_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_controller@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_controller@2x.png new file mode 100644 index 00000000..43fec0e9 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_controller@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_controller@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_controller@3x.png new file mode 100644 index 00000000..3771c641 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_controller@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer@2x.png new file mode 100644 index 00000000..0fd725d5 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer@3x.png new file mode 100644 index 00000000..cf10e3ab Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer_selected@2x.png new file mode 100644 index 00000000..3f8452ac Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer_selected@3x.png new file mode 100644 index 00000000..314108c3 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_gradientlayer_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview@2x.png new file mode 100644 index 00000000..6e444c2b Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview@3x.png new file mode 100644 index 00000000..ce1b7d4a Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview_selected@2x.png new file mode 100644 index 00000000..40d76dda Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview_selected@3x.png new file mode 100644 index 00000000..0c81c0ef Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_imageview_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_label@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_label@2x.png new file mode 100644 index 00000000..203afaed Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_label@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_label@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_label@3x.png new file mode 100644 index 00000000..589a70cc Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_label@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_label_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_label_selected@2x.png new file mode 100644 index 00000000..b6928962 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_label_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_label_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_label_selected@3x.png new file mode 100644 index 00000000..1142e012 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_label_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer@2x.png new file mode 100644 index 00000000..779bb8dc Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer@3x.png new file mode 100644 index 00000000..64bba6b8 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer_selected@2x.png new file mode 100644 index 00000000..e8ab24f9 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer_selected@3x.png new file mode 100644 index 00000000..b183adb3 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_layer_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar@2x.png new file mode 100644 index 00000000..16d29406 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar@3x.png new file mode 100644 index 00000000..681cc225 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar_selected@2x.png new file mode 100644 index 00000000..38324651 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar_selected@3x.png new file mode 100644 index 00000000..d99be236 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_navigationbar_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview@2x.png new file mode 100644 index 00000000..e38b03ba Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview@3x.png new file mode 100644 index 00000000..145c7333 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview_selected@2x.png new file mode 100644 index 00000000..f79c421f Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview_selected@3x.png new file mode 100644 index 00000000..bc0552d2 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_scrollview_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer@2x.png new file mode 100644 index 00000000..e5ded762 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer@3x.png new file mode 100644 index 00000000..0e306225 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer_selected@2x.png new file mode 100644 index 00000000..764295ab Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer_selected@3x.png new file mode 100644 index 00000000..b72c53d7 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_shapelayer_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider@2x.png new file mode 100644 index 00000000..16d1d6a4 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider@3x.png new file mode 100644 index 00000000..5acccf11 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider_selected@2x.png new file mode 100644 index 00000000..12ab7739 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider_selected@3x.png new file mode 100644 index 00000000..1c97033f Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_slider_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar@2x.png new file mode 100644 index 00000000..dcb1821f Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar@3x.png new file mode 100644 index 00000000..7842b256 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar_selected@2x.png new file mode 100644 index 00000000..e3e2b4fb Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar_selected@3x.png new file mode 100644 index 00000000..2c7f755d Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tabbar_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell@2x.png new file mode 100644 index 00000000..a52448d0 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell@3x.png new file mode 100644 index 00000000..6bcbc34b Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell_selected@2x.png new file mode 100644 index 00000000..0cc8db83 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell_selected@3x.png new file mode 100644 index 00000000..7416b3e6 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecell_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator@2x.png new file mode 100644 index 00000000..becb3fec Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator@3x.png new file mode 100644 index 00000000..f38f6f96 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator_selected@2x.png new file mode 100644 index 00000000..2453c45c Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator_selected@3x.png new file mode 100644 index 00000000..58a1f4b3 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tablecellseparator_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter@2x.png new file mode 100644 index 00000000..6846d742 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter@3x.png new file mode 100644 index 00000000..33492eef Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@2x.png new file mode 100644 index 00000000..2ae4228a Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@3x.png new file mode 100644 index 00000000..935fd6da Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview@2x.png new file mode 100644 index 00000000..5e20879d Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview@3x.png new file mode 100644 index 00000000..a787b2d4 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview_selected@2x.png new file mode 100644 index 00000000..0ea1e3a5 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview_selected@3x.png new file mode 100644 index 00000000..41d11cf9 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_tableview_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield@2x.png new file mode 100644 index 00000000..48f6638c Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield@3x.png new file mode 100644 index 00000000..b157d034 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield_selected@2x.png new file mode 100644 index 00000000..9a2dc621 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield_selected@3x.png new file mode 100644 index 00000000..5aa2d9ad Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textfield_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview@2x.png new file mode 100644 index 00000000..66a51fb3 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview@3x.png new file mode 100644 index 00000000..be147386 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview_selected@2x.png new file mode 100644 index 00000000..bc5bf2a1 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview_selected@3x.png new file mode 100644 index 00000000..be3e6719 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_textview_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_view@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_view@2x.png new file mode 100644 index 00000000..43e0b50a Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_view@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_view@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_view@3x.png new file mode 100644 index 00000000..b1375397 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_view@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_view_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_view_selected@2x.png new file mode 100644 index 00000000..392f48ad Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_view_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_view_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_view_selected@3x.png new file mode 100644 index 00000000..5550103f Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_view_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview@2x.png new file mode 100644 index 00000000..57cb226c Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview@3x.png new file mode 100644 index 00000000..e5f883de Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview_selected@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview_selected@2x.png new file mode 100644 index 00000000..f7b2926f Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview_selected@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview_selected@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview_selected@3x.png new file mode 100644 index 00000000..b772fab6 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_webview_selected@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_window@2x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_window@2x.png new file mode 100644 index 00000000..698cf86c Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_window@2x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_window@3x.png b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_window@3x.png new file mode 100644 index 00000000..67390d25 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/LookinServerImages.bundle/hierarchy_window@3x.png differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/Modules/module.modulemap b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/Modules/module.modulemap new file mode 100644 index 00000000..36ab6fbb --- /dev/null +++ b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module LookinServer { + umbrella header "LookinServer.h" + + export * + module * { export * } +} diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/_CodeSignature/CodeResources b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/_CodeSignature/CodeResources new file mode 100644 index 00000000..061c7dbb --- /dev/null +++ b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/_CodeSignature/CodeResources @@ -0,0 +1,1205 @@ + + + + + files + + Info.plist + + Y7rZaoOiJ9T3sS2YmuRQKy3WCkw= + + LookinServerImages.bundle/hierarchy_button@2x.png + + CzRHEVFmLWxI06p9b3mxdOfk5DY= + + LookinServerImages.bundle/hierarchy_button@3x.png + + oOR2wmfnzXNJT4ZhB/2N65MRK04= + + LookinServerImages.bundle/hierarchy_button_selected@2x.png + + 7qR+GWglVEaaOx+0OYjYxuFQdIQ= + + LookinServerImages.bundle/hierarchy_button_selected@3x.png + + M42KpvJS65TYtcgzTDFz+L8tFr4= + + LookinServerImages.bundle/hierarchy_cellcontent@2x.png + + ghRtqfu0IlqS0LYoEm0VkoAiJ28= + + LookinServerImages.bundle/hierarchy_cellcontent@3x.png + + aQsRyv+2m+puWfa3/tXbCNG2iXw= + + LookinServerImages.bundle/hierarchy_cellcontent_selected@2x.png + + XwguwnjNHdqVuPOeeMgmPAGrj9U= + + LookinServerImages.bundle/hierarchy_cellcontent_selected@3x.png + + vA9s6y9raTF4oHtU0WTrKbtNQDA= + + LookinServerImages.bundle/hierarchy_collectioncell@2x.png + + sIKQylsecwV/H0jcDU+o30QhpkY= + + LookinServerImages.bundle/hierarchy_collectioncell@3x.png + + 9UDykNODzAp/6NeUfxBXUK/Bqvo= + + LookinServerImages.bundle/hierarchy_collectioncell_selected@2x.png + + BB07au5gu7SdiVzXpmtD4Bk/Hlk= + + LookinServerImages.bundle/hierarchy_collectioncell_selected@3x.png + + Y5AeQtmU2E6X/ljkqw3PtCIp0es= + + LookinServerImages.bundle/hierarchy_collectionreuseview@2x.png + + Wyg0gCsS8tkeLwppWdvSHr5f4t8= + + LookinServerImages.bundle/hierarchy_collectionreuseview@3x.png + + uMwA7Kp3K5M0kZw4PWSKARnNxwg= + + LookinServerImages.bundle/hierarchy_collectionreuseview_selected@2x.png + + j0mIUP+VbqB7WrlY7Dj/l4vyvYU= + + LookinServerImages.bundle/hierarchy_collectionreuseview_selected@3x.png + + Gwl2VhM9841YcuLvARS57qOzKAg= + + LookinServerImages.bundle/hierarchy_collectionview@2x.png + + m//vOMm0OiOxG4QmUKu2KGxjzu8= + + LookinServerImages.bundle/hierarchy_collectionview@3x.png + + woOnZp4ZRAqicVSYz2UEs6v3E/U= + + LookinServerImages.bundle/hierarchy_collectionview_selected@2x.png + + IufQCHIqyG1vZMcXyFrYblaEZq0= + + LookinServerImages.bundle/hierarchy_collectionview_selected@3x.png + + vdfYZBSiu8bfgoXmmN3I6TMdU8Y= + + LookinServerImages.bundle/hierarchy_control@2x.png + + GmS9TL2Ic1Jmw6neZLldlib8x8w= + + LookinServerImages.bundle/hierarchy_control@3x.png + + R64nIp16U80vPcF57Ezz/YHh6AA= + + LookinServerImages.bundle/hierarchy_control_selected@2x.png + + zu8U47GumKX84eOXmJZ4otwmdAY= + + LookinServerImages.bundle/hierarchy_control_selected@3x.png + + Luf8ZaRPIKFM+poaf5tREtumN0Y= + + LookinServerImages.bundle/hierarchy_controller@2x.png + + FOhHC/ztwxyukgkjyzCD3bVvZlo= + + LookinServerImages.bundle/hierarchy_controller@3x.png + + mQHYqsAIoG92xM4jd8EY7EmfGPc= + + LookinServerImages.bundle/hierarchy_gradientlayer@2x.png + + tJ9Gt/oRhZkxM0TWI0LL8XFzKnQ= + + LookinServerImages.bundle/hierarchy_gradientlayer@3x.png + + 7FcWhX8VtYbFVSi90jp8DTOjxnA= + + LookinServerImages.bundle/hierarchy_gradientlayer_selected@2x.png + + HUDndtn7NFyd+A7D/UN30fgAWT4= + + LookinServerImages.bundle/hierarchy_gradientlayer_selected@3x.png + + TBalHLL8mvoGO5mIR5eEswEknVk= + + LookinServerImages.bundle/hierarchy_imageview@2x.png + + G7o7o6a/sCkv12VG0sXRiIPVmR0= + + LookinServerImages.bundle/hierarchy_imageview@3x.png + + ifI08VWgbyX/K32IPQK48ev0ksQ= + + LookinServerImages.bundle/hierarchy_imageview_selected@2x.png + + stSJi3SdL3T0kK+DBIx2b27xZHA= + + LookinServerImages.bundle/hierarchy_imageview_selected@3x.png + + xJwj8ggyG0SaWKnrNgvu1pEjTeQ= + + LookinServerImages.bundle/hierarchy_label@2x.png + + JVaEA4aWT2ZVKxLQeXA7wU+/xIQ= + + LookinServerImages.bundle/hierarchy_label@3x.png + + ssYBzX8UcbwLIUERpa4zCUWNhzM= + + LookinServerImages.bundle/hierarchy_label_selected@2x.png + + 0961w0mX5HyNpDf4qpPCRvTtPdA= + + LookinServerImages.bundle/hierarchy_label_selected@3x.png + + D8++dRWwgnPFfkw+Icym5llPxl8= + + LookinServerImages.bundle/hierarchy_layer@2x.png + + Oz5A7WlVCo35Y9EB1F7Xf3kZgb0= + + LookinServerImages.bundle/hierarchy_layer@3x.png + + dy+FllZolGsjCju+rvvriZ9TLNE= + + LookinServerImages.bundle/hierarchy_layer_selected@2x.png + + Q3LIQLF95VSDHo2PmqoX1Sstdfc= + + LookinServerImages.bundle/hierarchy_layer_selected@3x.png + + LERxuxnIiMMC9OcNesXSE5MgpmU= + + LookinServerImages.bundle/hierarchy_navigationbar@2x.png + + pEksMAvI1zqQGWZsoEEXm9lt3y4= + + LookinServerImages.bundle/hierarchy_navigationbar@3x.png + + /29ixBNKYY+rKeyA/iLLPqe+4lI= + + LookinServerImages.bundle/hierarchy_navigationbar_selected@2x.png + + lf/MA7EinJQbpnLzTwXcsoEcoWY= + + LookinServerImages.bundle/hierarchy_navigationbar_selected@3x.png + + M/WO4kUrxux/tejPjOn2+oUgwRU= + + LookinServerImages.bundle/hierarchy_scrollview@2x.png + + MxSzI7eoZY3p8M4K8kAPf6CkEHk= + + LookinServerImages.bundle/hierarchy_scrollview@3x.png + + 5KDqu7gNIy+zcm/SVftDvfPqGxY= + + LookinServerImages.bundle/hierarchy_scrollview_selected@2x.png + + vr2DUKsaYpXLExVvvRp98T0OkAM= + + LookinServerImages.bundle/hierarchy_scrollview_selected@3x.png + + GTs6bQUXQ1Lpy58ttO8CzLDYxSw= + + LookinServerImages.bundle/hierarchy_shapelayer@2x.png + + le8N0EAbh4Qz3ve8eWabaqdpql4= + + LookinServerImages.bundle/hierarchy_shapelayer@3x.png + + qkfDhcsA6VfPfKWmYS+q9TzvgAs= + + LookinServerImages.bundle/hierarchy_shapelayer_selected@2x.png + + htm7BE+NdxLUXlp4sR0u5TkUVCo= + + LookinServerImages.bundle/hierarchy_shapelayer_selected@3x.png + + 85jWz0Z1zJC6OhYT+0/FC1zaN/0= + + LookinServerImages.bundle/hierarchy_slider@2x.png + + 22ZJaPx6Hdnzi9a9yM23uDxwTH4= + + LookinServerImages.bundle/hierarchy_slider@3x.png + + E8ML7kPjn2jKm4Gl4AYKeu+Zpk8= + + LookinServerImages.bundle/hierarchy_slider_selected@2x.png + + 3/Iedkik60HBHFhqXWvU5IRn6kE= + + LookinServerImages.bundle/hierarchy_slider_selected@3x.png + + mW1hBoTmE6LP6IFtNkGemKAlITE= + + LookinServerImages.bundle/hierarchy_tabbar@2x.png + + PLwaicij3NuoYPEU53hZxh04VCo= + + LookinServerImages.bundle/hierarchy_tabbar@3x.png + + IJPK9DNeiHLxR3RRRc5DPE4Z784= + + LookinServerImages.bundle/hierarchy_tabbar_selected@2x.png + + tdeLn9waTHG5bDBl2AxGfTbQloU= + + LookinServerImages.bundle/hierarchy_tabbar_selected@3x.png + + bWA5ZesmoiaykUN5xulFE5GeEkw= + + LookinServerImages.bundle/hierarchy_tablecell@2x.png + + 1l+qgUAKdrXELF9sKflhrZoy/6g= + + LookinServerImages.bundle/hierarchy_tablecell@3x.png + + lGVWnN2/tcLC65hircaWRSBuO4M= + + LookinServerImages.bundle/hierarchy_tablecell_selected@2x.png + + +/3dK/VSOxnWkKOS04ujeYrjpUw= + + LookinServerImages.bundle/hierarchy_tablecell_selected@3x.png + + ifx7WRYjk2P9dkJjn12Y5cMou3c= + + LookinServerImages.bundle/hierarchy_tablecellseparator@2x.png + + FifwmC8F9bk7yfYUDrEJg8sKoOs= + + LookinServerImages.bundle/hierarchy_tablecellseparator@3x.png + + nvOYg0u5wC4Nbn4KBUXeJ/9iv40= + + LookinServerImages.bundle/hierarchy_tablecellseparator_selected@2x.png + + XGXz6q1+LbHIECiE7dAs1/nOueA= + + LookinServerImages.bundle/hierarchy_tablecellseparator_selected@3x.png + + Y3RxmbVj8hWsqIWCTJDn9Ut11a8= + + LookinServerImages.bundle/hierarchy_tableheaderfooter@2x.png + + j++4m1hsFfQZVEBnpQOLennC9EU= + + LookinServerImages.bundle/hierarchy_tableheaderfooter@3x.png + + 6Sqjheo2W2X6Ml94wqOO0AE00h8= + + LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@2x.png + + bFEqwTt057btu8JT7wqLIScdjuo= + + LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@3x.png + + Nk85+s0zJ+tBAMLvPbIwbF4WSwk= + + LookinServerImages.bundle/hierarchy_tableview@2x.png + + KsC1IPsTTeXzmOc+GqJPyCGHW3s= + + LookinServerImages.bundle/hierarchy_tableview@3x.png + + DhVV4A3PwIFQgA/5G6uY1IeWtTg= + + LookinServerImages.bundle/hierarchy_tableview_selected@2x.png + + jC6lr1EkkCanSEKuD8Dw36ylNLE= + + LookinServerImages.bundle/hierarchy_tableview_selected@3x.png + + JkIYhKF3XmEgf1KklLQ+OdEgqCs= + + LookinServerImages.bundle/hierarchy_textfield@2x.png + + jpFeAbMonMMqgD1steZFBEH1+DU= + + LookinServerImages.bundle/hierarchy_textfield@3x.png + + MQK+tlVPO28f7CHRfoZUCMBZQ1I= + + LookinServerImages.bundle/hierarchy_textfield_selected@2x.png + + nSOROu+9QOSkdUT6mqJox7/frf0= + + LookinServerImages.bundle/hierarchy_textfield_selected@3x.png + + 2rtvSbEyYB+QKIYqzZob6iwTVLw= + + LookinServerImages.bundle/hierarchy_textview@2x.png + + bmZx+TR+Am2kUSWl355IN0PGUtc= + + LookinServerImages.bundle/hierarchy_textview@3x.png + + MOO1mLoiGh+ZGF1JinOzhqIOjuU= + + LookinServerImages.bundle/hierarchy_textview_selected@2x.png + + /MFvOp7I46Lk/8JM/iKezvyx6Lw= + + LookinServerImages.bundle/hierarchy_textview_selected@3x.png + + wb8PVkiNXHnQ0f/QUZj+go+xpiQ= + + LookinServerImages.bundle/hierarchy_view@2x.png + + P4CXY43leCOOLZ7nusZ1LW+YKbw= + + LookinServerImages.bundle/hierarchy_view@3x.png + + T65baRwP02zeVRRXPxTBeo2lwUQ= + + LookinServerImages.bundle/hierarchy_view_selected@2x.png + + CbxPHO9EPj3BOmLQOfjfNyrdFUs= + + LookinServerImages.bundle/hierarchy_view_selected@3x.png + + 4K9LEevy30vAjWXHXSpBFJwNeic= + + LookinServerImages.bundle/hierarchy_webview@2x.png + + zQDNP3O2h2NITYXU4koafoMrgaQ= + + LookinServerImages.bundle/hierarchy_webview@3x.png + + BJgRZZEEJsjjm2I/E02SoeGYW3Q= + + LookinServerImages.bundle/hierarchy_webview_selected@2x.png + + kc9nlkczRlF6vl1d675OfCO49BE= + + LookinServerImages.bundle/hierarchy_webview_selected@3x.png + + HMZeAr1vBsHNR7Hv9FswcuYNUKc= + + LookinServerImages.bundle/hierarchy_window@2x.png + + e6PoNkMCWFBXVs8akQ9prVdQwQo= + + LookinServerImages.bundle/hierarchy_window@3x.png + + 5tWq8Yjv1AoaA6CTwPts0tY1xhs= + + Modules/module.modulemap + + hvFWqznhiwNJZ/Mbvv2Ksu1C/h8= + + en.lproj/Localizable.strings + + hash + + Eu+I9a7jJw3iiV799jjvnedxuQw= + + optional + + + zh-Hans.lproj/Localizable.strings + + hash + + OqaH2Dk5jVlrmHcwkwwfqEG5ctE= + + optional + + + + files2 + + LookinServerImages.bundle/hierarchy_button@2x.png + + hash2 + + UcADEOZBTcxKqwr/C9m1zjabjwiEyH8p3P/a7H8MR6o= + + + LookinServerImages.bundle/hierarchy_button@3x.png + + hash2 + + QQLlwcOXKSQDQ2N8Kr+RvtPpaYiD9+0PpMuTNL1lQ34= + + + LookinServerImages.bundle/hierarchy_button_selected@2x.png + + hash2 + + PJ1b1Wx8dXbdmE/a4BqRSlJh7FWUkQUiNJuANe+io2M= + + + LookinServerImages.bundle/hierarchy_button_selected@3x.png + + hash2 + + pQmmqb5Ou3K4QOIousIVo8T28tU7Jg77YiNCWbyTIJU= + + + LookinServerImages.bundle/hierarchy_cellcontent@2x.png + + hash2 + + tRVS6SoDUJ9N8SJXU2q6Jr3KdjG5u9lGZisPbYOMvxQ= + + + LookinServerImages.bundle/hierarchy_cellcontent@3x.png + + hash2 + + yPI4g+fUrvci5vk6XwkamJPQUMzylQS87GSaMkz/2mw= + + + LookinServerImages.bundle/hierarchy_cellcontent_selected@2x.png + + hash2 + + ml/jDXOpK7zPF95LB2FvCUs8luTI6Auty72TYaIkF74= + + + LookinServerImages.bundle/hierarchy_cellcontent_selected@3x.png + + hash2 + + KpIuIt57/GoFmGwPhfo4WrCRwPprWUbxsext9Yz98hA= + + + LookinServerImages.bundle/hierarchy_collectioncell@2x.png + + hash2 + + SKFWmFDOpiV2gIo6xMg+I7PWVegUTifzwXFPTHE+lNo= + + + LookinServerImages.bundle/hierarchy_collectioncell@3x.png + + hash2 + + q/pAWFB+IZioKQar/6H04VBg9uw1vEb5p8f35QFyZUs= + + + LookinServerImages.bundle/hierarchy_collectioncell_selected@2x.png + + hash2 + + lphI2I9XZHtgWHF4jSC1f2L00NpiPLf/1BJwr4JrfCk= + + + LookinServerImages.bundle/hierarchy_collectioncell_selected@3x.png + + hash2 + + 2ZI/L/N/SxGj5y4uMAVAeKi8iWR3UsS6v6CdSd53+/I= + + + LookinServerImages.bundle/hierarchy_collectionreuseview@2x.png + + hash2 + + dzCXULP/sqeiJO1r0cR+tRU59rsDnhsFzNGru7vNwG8= + + + LookinServerImages.bundle/hierarchy_collectionreuseview@3x.png + + hash2 + + Puw1en6LA8pp0KCj1BCuASuWr9A0zOLpKYxhywL+/d4= + + + LookinServerImages.bundle/hierarchy_collectionreuseview_selected@2x.png + + hash2 + + gX4b09diaFRLKUUd0V1BYgM2QdPpZmmD9TQyORFE9DM= + + + LookinServerImages.bundle/hierarchy_collectionreuseview_selected@3x.png + + hash2 + + bbzto7u8F/FEVkxVEMnTv8HDn+BiJh7HzMrWoBSltIg= + + + LookinServerImages.bundle/hierarchy_collectionview@2x.png + + hash2 + + QZtLw+sSAdAgkqHzLPU4tnz+wzsFk9bk3ih6BIIUPRY= + + + LookinServerImages.bundle/hierarchy_collectionview@3x.png + + hash2 + + QG1RzPsr6q5IKTac7Ql+6N9++kYex+LrutbuNIcJ3Gw= + + + LookinServerImages.bundle/hierarchy_collectionview_selected@2x.png + + hash2 + + GFamybmGUb0WWGsHBHHIEeNLOmF/2Q17N7F7TCemY+g= + + + LookinServerImages.bundle/hierarchy_collectionview_selected@3x.png + + hash2 + + GdgAxCO4gyd1y8NXW1KdOSUqWP5C57UZa/yoCLkg2Pw= + + + LookinServerImages.bundle/hierarchy_control@2x.png + + hash2 + + pRWGjqiRLyWbkW4NxJ5rdmZEm+XXvV0pUWtBf17rhkY= + + + LookinServerImages.bundle/hierarchy_control@3x.png + + hash2 + + uxLktspbwM9NH20Cb1a1jtE53U+IRyAdCCtvi+rvmuY= + + + LookinServerImages.bundle/hierarchy_control_selected@2x.png + + hash2 + + mskNDhBAwBEhKUuWbvrnNfFIclFu/O39T7ekt3ea9rk= + + + LookinServerImages.bundle/hierarchy_control_selected@3x.png + + hash2 + + BmBbQh9c+KR1dRNkv9mDGusH6vt29PyFLZXud/0AXYM= + + + LookinServerImages.bundle/hierarchy_controller@2x.png + + hash2 + + dB4Vsrb7t4DzNdamMq8EOIultgJCMGmyduhryNEw7ak= + + + LookinServerImages.bundle/hierarchy_controller@3x.png + + hash2 + + x0kFUESSEPBVKh6Hl0x1LPEX0brBUb0wpjAxFt0FuNI= + + + LookinServerImages.bundle/hierarchy_gradientlayer@2x.png + + hash2 + + ijxWZYwDIGs9WdOBZi3pI3LkEbs4gXgHg3JxLPG3Ffk= + + + LookinServerImages.bundle/hierarchy_gradientlayer@3x.png + + hash2 + + jIodMpu3ajOLQPjUJ271zpB6rcKkuyePT+Ipbe+OevU= + + + LookinServerImages.bundle/hierarchy_gradientlayer_selected@2x.png + + hash2 + + fURAaOchMGsX9jGvmiSrPjaVC9/7a5vnAYrTvpfPSdo= + + + LookinServerImages.bundle/hierarchy_gradientlayer_selected@3x.png + + hash2 + + KhAWLL0zzJyf5GSjHS/AlMrhJBrSrvzBiWLduFY2dv8= + + + LookinServerImages.bundle/hierarchy_imageview@2x.png + + hash2 + + tnwBrczsv9ni8aMst518OTUxsM0AMTvIdceZSu39Tq4= + + + LookinServerImages.bundle/hierarchy_imageview@3x.png + + hash2 + + RUq1hVLnT+FwVG0FuNm+7UQht6T3du0baBXBMdQHplY= + + + LookinServerImages.bundle/hierarchy_imageview_selected@2x.png + + hash2 + + B3yiawkmvjVqe0VUvON6L2ect/eC1JuYJkiM4gjXU40= + + + LookinServerImages.bundle/hierarchy_imageview_selected@3x.png + + hash2 + + van0XJhMM4tsFiCpvx8Lzs9aR0m5IbX5It09M1OHDtc= + + + LookinServerImages.bundle/hierarchy_label@2x.png + + hash2 + + c7+iSm6YzRDoTuCk5r3LcYweKj6DDiDYx8/84ItyrFk= + + + LookinServerImages.bundle/hierarchy_label@3x.png + + hash2 + + j8Fh4OhHIaiMY1y8h24AQV/pPxYVU5epVOcB75vZG0Y= + + + LookinServerImages.bundle/hierarchy_label_selected@2x.png + + hash2 + + qKJxnon3+4q3b95IFStZ7dvM60NZ9ILvouHzutsUjAM= + + + LookinServerImages.bundle/hierarchy_label_selected@3x.png + + hash2 + + 9a2r/XA1+QDqoPWUTedVbS0m/WxmN3sBQQj+fcnUQ1o= + + + LookinServerImages.bundle/hierarchy_layer@2x.png + + hash2 + + O8WEaawVn1Z1vgFdeF8n1TZI6KtKZwhB6s6TkulDpDE= + + + LookinServerImages.bundle/hierarchy_layer@3x.png + + hash2 + + SyKWAPQi9ZuARpeMNOHBQNKOzSw6LgFTJJTt/jUcguw= + + + LookinServerImages.bundle/hierarchy_layer_selected@2x.png + + hash2 + + zzA0wClNzN/z2RJPWzlW4nJKozjn0vUxeoAV7o3yVBY= + + + LookinServerImages.bundle/hierarchy_layer_selected@3x.png + + hash2 + + YZ4vzyTa4J9OeQ6yFdfvm1K+eetMlG2gzP9kSwdrQtA= + + + LookinServerImages.bundle/hierarchy_navigationbar@2x.png + + hash2 + + ISIpbKI8YuZ9A+bOePRMn6i7W1u3FvOgV5kb7v9ZINk= + + + LookinServerImages.bundle/hierarchy_navigationbar@3x.png + + hash2 + + BWIVziGe5SzpzbUL6zZLlDUy02GbZjzgzDP+3B2mMuk= + + + LookinServerImages.bundle/hierarchy_navigationbar_selected@2x.png + + hash2 + + 2JjCJl8t+R4VpOAdrGOWE1q9DcbVvhg726gAO0b/SvY= + + + LookinServerImages.bundle/hierarchy_navigationbar_selected@3x.png + + hash2 + + W+bCgwrYOukR3q/TV63knjyjMai5CL8zInvi1JD/K6c= + + + LookinServerImages.bundle/hierarchy_scrollview@2x.png + + hash2 + + +IzSgRGOqBuaisl3C9pcTY3uCinUophyJeH61I8vKAQ= + + + LookinServerImages.bundle/hierarchy_scrollview@3x.png + + hash2 + + 7C8lMQcLqqG0d6qUs8Pj7iWE+RGORaLbGu4AlGbaJFo= + + + LookinServerImages.bundle/hierarchy_scrollview_selected@2x.png + + hash2 + + aMMft9fZXmWan1weO9DwXpGL487EOmXUVn7RAOI+REQ= + + + LookinServerImages.bundle/hierarchy_scrollview_selected@3x.png + + hash2 + + DYAyQYPTdE7IW31z4DL/zhb8MJiLrn00AJfJJpvr9Rk= + + + LookinServerImages.bundle/hierarchy_shapelayer@2x.png + + hash2 + + ejnJ8m3gDUfYh7HL1RARawfBXiqdbIw5xlK6QtwEkOA= + + + LookinServerImages.bundle/hierarchy_shapelayer@3x.png + + hash2 + + nYsi7jFyq+3CXOoYKYXLHz74K7C1gIQ4ChVUpSEJi1I= + + + LookinServerImages.bundle/hierarchy_shapelayer_selected@2x.png + + hash2 + + 73SCFqMtdx3PmXnARiKYlNaEZk4khIMziuOdPUFMsks= + + + LookinServerImages.bundle/hierarchy_shapelayer_selected@3x.png + + hash2 + + mCEHzue5ejhMGZIyth3wF3rGh2RqNTBYKB5Xnqq/5+8= + + + LookinServerImages.bundle/hierarchy_slider@2x.png + + hash2 + + lgZE42LYYMc4YsAs/knM1GjiSJawx5jkC2CGyqxw5r0= + + + LookinServerImages.bundle/hierarchy_slider@3x.png + + hash2 + + AjuURPcc2KkOzMVo05GGP/yVWy7pgqJTLFMAcugiLow= + + + LookinServerImages.bundle/hierarchy_slider_selected@2x.png + + hash2 + + U7fRMKM0Qoz7oyCJKC9tDlZc82u5pwI9d/aj7PnnNzE= + + + LookinServerImages.bundle/hierarchy_slider_selected@3x.png + + hash2 + + VG0Lhw6RVyVaLBkXZwlRcdFnUQDk9wVELwBO+nA3Bzw= + + + LookinServerImages.bundle/hierarchy_tabbar@2x.png + + hash2 + + kbl70ULigteOYGdPZ1wHHbLFsHrpTBts8LWzzINic88= + + + LookinServerImages.bundle/hierarchy_tabbar@3x.png + + hash2 + + JOlCPGbxAsg1Dl3Aqj291tdM62C/AqOw+kv48CY6YBc= + + + LookinServerImages.bundle/hierarchy_tabbar_selected@2x.png + + hash2 + + qJ0q1WcNNLEew0yBjHu6Cw2l7tuDZ4th7LwPZTIR8yo= + + + LookinServerImages.bundle/hierarchy_tabbar_selected@3x.png + + hash2 + + XTYiBy6Qzsdg4NEvbeX39KBNsyPFFrdoMHFsm5v4/Ts= + + + LookinServerImages.bundle/hierarchy_tablecell@2x.png + + hash2 + + /PTr+DyeaYFuJ6rzJakTEcApFAHmk6CkrZAm7XXxqMc= + + + LookinServerImages.bundle/hierarchy_tablecell@3x.png + + hash2 + + I/QA9hUS0zabZ09CNcX7MzIT80YXVwLMwisEuwTpVSE= + + + LookinServerImages.bundle/hierarchy_tablecell_selected@2x.png + + hash2 + + HpMSirsUYySJVEemMB8XQkJQYj8H4Hcdy4dIDnpQHhE= + + + LookinServerImages.bundle/hierarchy_tablecell_selected@3x.png + + hash2 + + SmA0BY6tlu06RalNVTvfIKgYTuQFTt+Qw9bcAG1hErA= + + + LookinServerImages.bundle/hierarchy_tablecellseparator@2x.png + + hash2 + + bpHZpQE6Kdsj+cnuEuCXNLXdlZ8dHEpaPpPaveAq8ac= + + + LookinServerImages.bundle/hierarchy_tablecellseparator@3x.png + + hash2 + + h4KWgE8utF8h33c3RJzYnvbpen6cjyJTbWci9bb8qvI= + + + LookinServerImages.bundle/hierarchy_tablecellseparator_selected@2x.png + + hash2 + + eJvftSxkRvJPQh8PFiD2OekOWRhiDy18RdQkPxJ/EsE= + + + LookinServerImages.bundle/hierarchy_tablecellseparator_selected@3x.png + + hash2 + + BuKTxZOPuTlAG+FAKmY5q91dWuY/cZcRSDgiSZ+634s= + + + LookinServerImages.bundle/hierarchy_tableheaderfooter@2x.png + + hash2 + + 65FzyeJAsO0UmMBwNn/mXehEaNQok/hahyStD3NdJBY= + + + LookinServerImages.bundle/hierarchy_tableheaderfooter@3x.png + + hash2 + + Nn9Qarw/1GPhee6iIBWjiWUJT0U7fA7PBBP3SzBEZIM= + + + LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@2x.png + + hash2 + + pDJuQBLLNFCj0BDxXJEP9h5yeXghM4YIRT+/T4gIUJg= + + + LookinServerImages.bundle/hierarchy_tableheaderfooter_selected@3x.png + + hash2 + + AFImA0R7uy7m0RqaF/Xt5+V0YcwsKZY60Ubks+fZesQ= + + + LookinServerImages.bundle/hierarchy_tableview@2x.png + + hash2 + + G0itfs2bvtWe+qYJaYBJ03NhnQn8UYypVrC11i4EHLA= + + + LookinServerImages.bundle/hierarchy_tableview@3x.png + + hash2 + + WxK+/7g2KkXXIMjP/QEd8VsAokCFLfktuLYTISe3jvY= + + + LookinServerImages.bundle/hierarchy_tableview_selected@2x.png + + hash2 + + 76ZhmRXJgmvEYM4qQ9rdzIeasptAkZCWID0UiMQ6Gng= + + + LookinServerImages.bundle/hierarchy_tableview_selected@3x.png + + hash2 + + LgyxM3+5MdHELeXZB5/tcwyK75CSC3yL/z/yewO9RvA= + + + LookinServerImages.bundle/hierarchy_textfield@2x.png + + hash2 + + zRjwID6joSQz3/PtnL10KvvQgM+uYcpo2dwvxg7rPEA= + + + LookinServerImages.bundle/hierarchy_textfield@3x.png + + hash2 + + +luB46Q7Te56VfKP0RJP+mQZfIJeKsl1HwmlKef9Ywc= + + + LookinServerImages.bundle/hierarchy_textfield_selected@2x.png + + hash2 + + qIj//Tu6JdC7giY0T+5qMtrln83zb97Zmh8zQ2qaAZ4= + + + LookinServerImages.bundle/hierarchy_textfield_selected@3x.png + + hash2 + + RBzMQDSz7lofF/ZCWxvXSV4qfeaQaNHF9Bz+9cRfPL8= + + + LookinServerImages.bundle/hierarchy_textview@2x.png + + hash2 + + CXWLs7VelcDR30hYu3waj0wZnNzRNBY9eElJgU8n2pQ= + + + LookinServerImages.bundle/hierarchy_textview@3x.png + + hash2 + + VDARQ93aVTUprGnKv1NihxCj5mDqObYQ3d+XOm96Zm8= + + + LookinServerImages.bundle/hierarchy_textview_selected@2x.png + + hash2 + + F3FvyCjsUQVG5add1BUICNrZgeo+cIkjR6q/o5EySkg= + + + LookinServerImages.bundle/hierarchy_textview_selected@3x.png + + hash2 + + uk6rCqRu+yZby4cN7taf5IoliAwHAuZZPiZw2ZISbZs= + + + LookinServerImages.bundle/hierarchy_view@2x.png + + hash2 + + DSAHGfqg3LL53WEY6sBEQS43lzaAnl1FtqWIm5PA/2o= + + + LookinServerImages.bundle/hierarchy_view@3x.png + + hash2 + + +/1/nT4dyULWDFHgD1V16zS+bVpyrCwqhs9pZGx6FmE= + + + LookinServerImages.bundle/hierarchy_view_selected@2x.png + + hash2 + + YfYpNSYFeKZBuz66b5Wadyg8vbG9dX/E144ozzEVJjY= + + + LookinServerImages.bundle/hierarchy_view_selected@3x.png + + hash2 + + SZEFFAJXIbVf9BzthkAIqqROXUKR5yQ2FeG2wHMvSNU= + + + LookinServerImages.bundle/hierarchy_webview@2x.png + + hash2 + + gcqVw15B4Sv3Y01UNvaytXLK2KyDMY3DOH27YWV730Y= + + + LookinServerImages.bundle/hierarchy_webview@3x.png + + hash2 + + N8dBBt3X61aS/N2RoSoncsBalVMNYuanGiSGOp/JVBU= + + + LookinServerImages.bundle/hierarchy_webview_selected@2x.png + + hash2 + + mmjqj+wFBBwZqGkja5pPXzJLBO26nyIYYjvkTJIUfLU= + + + LookinServerImages.bundle/hierarchy_webview_selected@3x.png + + hash2 + + nCdZ2AZwujx4s90iHea9egmSRHNye8NH6eWu1RDUDow= + + + LookinServerImages.bundle/hierarchy_window@2x.png + + hash2 + + h4LEfgqGmtGb9cPBzbkMl+wYaoWXdHZG3imtVyImYHc= + + + LookinServerImages.bundle/hierarchy_window@3x.png + + hash2 + + L4fi6D5oVFRjNHeJHjYS7AGRhD1yYUfAvA1jqXHfHpU= + + + Modules/module.modulemap + + hash2 + + 20VXDGRrp4NTN6MUIoq+F0Y4oVr5nUBn2Ey1qwI/3Us= + + + en.lproj/Localizable.strings + + hash2 + + ZpIRgEZ7f1fuxH20jpl4cn4T5KQnM7TWpHXtBbKxxuM= + + optional + + + zh-Hans.lproj/Localizable.strings + + hash2 + + ROjkDRHEJsdmu6N7A5clC0RW4SYuwAu8Uf8bhSB+DaQ= + + optional + + + + rules + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/en.lproj/Localizable.strings b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/en.lproj/Localizable.strings new file mode 100644 index 00000000..53b6f85e Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/en.lproj/Localizable.strings differ diff --git a/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/zh-Hans.lproj/Localizable.strings b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/zh-Hans.lproj/Localizable.strings new file mode 100644 index 00000000..9f2320b4 Binary files /dev/null and b/qmuidemo/Frameworks/LookinServer.xcframework/ios-arm64_x86_64-simulator/LookinServer.framework/zh-Hans.lproj/Localizable.strings differ diff --git a/qmuidemo/Frameworks/RevealServer.framework/Headers/RevealServer.h b/qmuidemo/Frameworks/RevealServer.framework/Headers/RevealServer.h deleted file mode 100644 index 117784bf..00000000 --- a/qmuidemo/Frameworks/RevealServer.framework/Headers/RevealServer.h +++ /dev/null @@ -1,11 +0,0 @@ -// -// Copyright © 2015 Itty Bitty Apps, Pty Ltd. All rights reserved. - -#import - -//! Project version number for RevealServer. -FOUNDATION_EXPORT double RevealServerVersionNumber; - -//! Project version string for RevealServer. -FOUNDATION_EXPORT const unsigned char RevealServerVersionString[]; - diff --git a/qmuidemo/Frameworks/RevealServer.framework/Info.plist b/qmuidemo/Frameworks/RevealServer.framework/Info.plist deleted file mode 100644 index f61f0d93..00000000 --- a/qmuidemo/Frameworks/RevealServer.framework/Info.plist +++ /dev/null @@ -1,53 +0,0 @@ - - - - - BuildMachineOSBuild - 18B75 - CFBundleDevelopmentRegion - en - CFBundleExecutable - RevealServer - CFBundleIdentifier - com.ittybittyapps.RevealServer - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - RevealServer - CFBundlePackageType - FMWK - CFBundleShortVersionString - 19 - CFBundleSignature - ???? - CFBundleSupportedPlatforms - - iPhoneOS - - CFBundleVersion - 11309 - DTCompiler - com.apple.compilers.llvm.clang.1_0 - DTPlatformBuild - 16B91 - DTPlatformName - iphoneos - DTPlatformVersion - 12.1 - DTSDKBuild - 16B91 - DTSDKName - iphoneos12.1 - DTXcode - 1010 - DTXcodeBuild - 10B61 - MinimumOSVersion - 8.0 - UIDeviceFamily - - 1 - 2 - - - diff --git a/qmuidemo/Frameworks/RevealServer.framework/Modules/module.modulemap b/qmuidemo/Frameworks/RevealServer.framework/Modules/module.modulemap deleted file mode 100644 index 8b2d46c6..00000000 --- a/qmuidemo/Frameworks/RevealServer.framework/Modules/module.modulemap +++ /dev/null @@ -1,6 +0,0 @@ -framework module RevealServer { - umbrella header "RevealServer.h" - - export * - module * { export * } -} diff --git a/qmuidemo/Frameworks/RevealServer.framework/RevealServer b/qmuidemo/Frameworks/RevealServer.framework/RevealServer deleted file mode 100755 index 27931c94..00000000 Binary files a/qmuidemo/Frameworks/RevealServer.framework/RevealServer and /dev/null differ diff --git a/qmuidemo/Frameworks/RevealServer.framework/Scripts/copy_and_codesign_revealserver.sh b/qmuidemo/Frameworks/RevealServer.framework/Scripts/copy_and_codesign_revealserver.sh deleted file mode 100755 index 5be5670b..00000000 --- a/qmuidemo/Frameworks/RevealServer.framework/Scripts/copy_and_codesign_revealserver.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash -set -o errexit -set -o nounset - -# Ensure that we have a valid OTHER_LDFLAGS environment variable -OTHER_LDFLAGS=${OTHER_LDFLAGS:=""} - -# Ensure that we have a valid REVEAL_SERVER_FILENAME environment variable -REVEAL_SERVER_FILENAME=${REVEAL_SERVER_FILENAME:="RevealServer.framework"} - -# Ensure that we have a valid REVEAL_SERVER_PATH environment variable -REVEAL_SERVER_PATH=${REVEAL_SERVER_PATH:="${SRCROOT}/${REVEAL_SERVER_FILENAME}"} - -# The path to copy the framework to -app_frameworks_dir="${CODESIGNING_FOLDER_PATH}/Frameworks" - -copy_library() { - mkdir -p "$app_frameworks_dir" - cp -vRf "$REVEAL_SERVER_PATH" "${app_frameworks_dir}/" -} - -codesign_library() { - if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" ]; then - codesign -fs "${EXPANDED_CODE_SIGN_IDENTITY}" "${app_frameworks_dir}/${REVEAL_SERVER_FILENAME}" - fi -} - -main() { - if [[ $OTHER_LDFLAGS =~ "RevealServer" ]]; then - if [ -e "$REVEAL_SERVER_PATH" ]; then - copy_library - codesign_library - echo "${REVEAL_SERVER_FILENAME} is included in this build, and has been copied to $CODESIGNING_FOLDER_PATH" - else - echo "${REVEAL_SERVER_FILENAME} is not included in this build, as it could not be found at $REVEAL_SERVER_PATH" - fi - else - echo "${REVEAL_SERVER_FILENAME} is not included in this build because RevealServer was not present in the OTHER_LDFLAGS environment variable." - fi -} - -main diff --git a/qmuidemo/Frameworks/RevealServer.framework/_CodeSignature/CodeResources b/qmuidemo/Frameworks/RevealServer.framework/_CodeSignature/CodeResources deleted file mode 100644 index ac8737d0..00000000 --- a/qmuidemo/Frameworks/RevealServer.framework/_CodeSignature/CodeResources +++ /dev/null @@ -1,147 +0,0 @@ - - - - - files - - Headers/RevealServer.h - - JavE2LI6bNGfH9W90/bDxwIxVlg= - - Info.plist - - Gqito7gsoepwuyaFqyMgjCpO4qY= - - Modules/module.modulemap - - EuDEeG1dcC1sd+hIW2SkUAImUg8= - - Scripts/copy_and_codesign_revealserver.sh - - yB2zXTggmRD0rra2Hbpxn2zfCRo= - - - files2 - - Headers/RevealServer.h - - hash - - JavE2LI6bNGfH9W90/bDxwIxVlg= - - hash2 - - weLK/65HyV1EiL9dd4xoAqQhf0/0r/Oy6os2nXCA+Ew= - - - Modules/module.modulemap - - hash - - EuDEeG1dcC1sd+hIW2SkUAImUg8= - - hash2 - - tstqiJpIPr4iEd3MDHClLuTB/ciSC/zNlke1AjfSVuU= - - - Scripts/copy_and_codesign_revealserver.sh - - hash - - yB2zXTggmRD0rra2Hbpxn2zfCRo= - - hash2 - - SL0x5cGSOyS1RcojHFEmFX5IDfon/vO37cFsjSIX7LA= - - - - rules - - ^.* - - ^.*\.lproj/ - - optional - - weight - 1000 - - ^.*\.lproj/locversion.plist$ - - omit - - weight - 1100 - - ^Base\.lproj/ - - weight - 1010 - - ^version.plist$ - - - rules2 - - .*\.dSYM($|/) - - weight - 11 - - ^(.*/)?\.DS_Store$ - - omit - - weight - 2000 - - ^.* - - ^.*\.lproj/ - - optional - - weight - 1000 - - ^.*\.lproj/locversion.plist$ - - omit - - weight - 1100 - - ^Base\.lproj/ - - weight - 1010 - - ^Info\.plist$ - - omit - - weight - 20 - - ^PkgInfo$ - - omit - - weight - 20 - - ^embedded\.provisionprofile$ - - weight - 20 - - ^version\.plist$ - - weight - 20 - - - - diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Contents.json b/qmuidemo/Images.xcassets/AppIcon.appiconset/Contents.json index 610bf7f5..d5932d15 100644 --- a/qmuidemo/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/qmuidemo/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,200 +1,38 @@ { "images" : [ { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-20@2x-1.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-Small-20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-Small-29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-Small-29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-Small-40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-Small-40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-20.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-Small-29.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-Small-29@2x-1.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-40.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-Small-40@2x-1.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-76.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-1024.png", - "scale" : "1x" - }, - { - "size" : "24x24", - "idiom" : "watch", - "filename" : "Icon-24@2x.png", - "scale" : "2x", - "role" : "notificationCenter", - "subtype" : "38mm" - }, - { - "size" : "27.5x27.5", - "idiom" : "watch", - "filename" : "Icon-27.5@2x.png", - "scale" : "2x", - "role" : "notificationCenter", - "subtype" : "42mm" - }, - { - "size" : "29x29", - "idiom" : "watch", - "filename" : "Icon-29@2x.png", - "role" : "companionSettings", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "watch", - "filename" : "Icon-29@3x.png", - "role" : "companionSettings", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "watch", - "filename" : "Icon-40@2x.png", - "scale" : "2x", - "role" : "appLauncher", - "subtype" : "38mm" - }, - { - "size" : "44x44", - "idiom" : "watch", - "scale" : "2x", - "role" : "appLauncher", - "subtype" : "40mm" - }, - { - "size" : "50x50", - "idiom" : "watch", - "scale" : "2x", - "role" : "appLauncher", - "subtype" : "44mm" - }, - { - "size" : "86x86", - "idiom" : "watch", - "filename" : "Icon-86@2x.png", - "scale" : "2x", - "role" : "quickLook", - "subtype" : "38mm" - }, - { - "size" : "98x98", - "idiom" : "watch", - "filename" : "Icon-98@2x.png", - "scale" : "2x", - "role" : "quickLook", - "subtype" : "42mm" - }, - { - "size" : "108x108", - "idiom" : "watch", - "scale" : "2x", - "role" : "quickLook", - "subtype" : "44mm" - }, - { - "size" : "1024x1024", - "idiom" : "watch-marketing", - "filename" : "Icon-1025.png", - "scale" : "1x" + "filename" : "app_logo.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "app_logo_dark.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "filename" : "Icon_tinted.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" } ], "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "pre-rendered" : true + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-1024.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-1024.png deleted file mode 100644 index 84c1cbb3..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-1024.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-1025.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-1025.png deleted file mode 100644 index 11748dba..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-1025.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-20.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-20.png deleted file mode 100644 index 72680254..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-20.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-20@2x-1.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-20@2x-1.png deleted file mode 100644 index ec7b15e1..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-20@2x-1.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-20@2x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-20@2x.png deleted file mode 100644 index ec7b15e1..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-20@2x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-24@2x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-24@2x.png deleted file mode 100644 index a186eae5..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-24@2x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-27.5@2x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-27.5@2x.png deleted file mode 100644 index a8bd9d6c..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-27.5@2x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-29@2x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-29@2x.png deleted file mode 100644 index 545ee44c..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-29@2x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-29@3x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-29@3x.png deleted file mode 100644 index 7c4b4621..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-29@3x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-40.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-40.png deleted file mode 100644 index ec7b15e1..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-40.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png deleted file mode 100644 index 09c2bfc8..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png deleted file mode 100644 index 1e567ba9..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png deleted file mode 100644 index 06395f92..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-76.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-76.png deleted file mode 100644 index f5bf1294..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-76.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png deleted file mode 100644 index 069a0857..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png deleted file mode 100644 index 1125c520..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-86@2x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-86@2x.png deleted file mode 100644 index bc5d492c..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-86@2x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-98@2x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-98@2x.png deleted file mode 100644 index c6d0ea67..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-98@2x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-20@3x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-20@3x.png deleted file mode 100644 index e9a625b5..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-20@3x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-29.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-29.png deleted file mode 100644 index cb59145c..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-29.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-29@2x-1.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-29@2x-1.png deleted file mode 100644 index 9e371fe0..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-29@2x-1.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-29@2x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-29@2x.png deleted file mode 100644 index 9e371fe0..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-29@2x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-29@3x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-29@3x.png deleted file mode 100644 index af96225e..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-29@3x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png deleted file mode 100644 index 654444dc..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png deleted file mode 100644 index 654444dc..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png deleted file mode 100644 index 1e567ba9..00000000 Binary files a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png and /dev/null differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon_tinted.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon_tinted.png new file mode 100644 index 00000000..3f1f0102 Binary files /dev/null and b/qmuidemo/Images.xcassets/AppIcon.appiconset/Icon_tinted.png differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/app_logo.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/app_logo.png new file mode 100644 index 00000000..fc98f354 Binary files /dev/null and b/qmuidemo/Images.xcassets/AppIcon.appiconset/app_logo.png differ diff --git a/qmuidemo/Images.xcassets/AppIcon.appiconset/app_logo_dark.png b/qmuidemo/Images.xcassets/AppIcon.appiconset/app_logo_dark.png new file mode 100644 index 00000000..42a00e68 Binary files /dev/null and b/qmuidemo/Images.xcassets/AppIcon.appiconset/app_logo_dark.png differ diff --git a/qmuidemo/Images.xcassets/Contents.json b/qmuidemo/Images.xcassets/Contents.json index da4a164c..73c00596 100644 --- a/qmuidemo/Images.xcassets/Contents.json +++ b/qmuidemo/Images.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/qmuidemo/Images.xcassets/about_logo_monochrome.imageset/Contents.json b/qmuidemo/Images.xcassets/about_logo_monochrome.imageset/Contents.json index 83d94bb0..8fdbd14f 100644 --- a/qmuidemo/Images.xcassets/about_logo_monochrome.imageset/Contents.json +++ b/qmuidemo/Images.xcassets/about_logo_monochrome.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "about_logo_monochrome.pdf" + "filename" : "about_logo_monochrome.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/qmuidemo/Images.xcassets/about_logo_monochrome.imageset/about_logo_monochrome.pdf b/qmuidemo/Images.xcassets/about_logo_monochrome.imageset/about_logo_monochrome.pdf index 1d23c586..db3027b4 100644 Binary files a/qmuidemo/Images.xcassets/about_logo_monochrome.imageset/about_logo_monochrome.pdf and b/qmuidemo/Images.xcassets/about_logo_monochrome.imageset/about_logo_monochrome.pdf differ diff --git a/qmuidemo/Images.xcassets/icon_grid_blur.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_grid_blur.imageset/Contents.json new file mode 100644 index 00000000..356cdcd6 --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_grid_blur.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon_grid_blur.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/qmuidemo/Images.xcassets/icon_grid_blur.imageset/icon_grid_blur.pdf b/qmuidemo/Images.xcassets/icon_grid_blur.imageset/icon_grid_blur.pdf new file mode 100644 index 00000000..7de2997c Binary files /dev/null and b/qmuidemo/Images.xcassets/icon_grid_blur.imageset/icon_grid_blur.pdf differ diff --git a/qmuidemo/Images.xcassets/icon_grid_checkbox.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_grid_checkbox.imageset/Contents.json new file mode 100644 index 00000000..69a1aa62 --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_grid_checkbox.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon_grid_checkbox.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/qmuidemo/Images.xcassets/icon_grid_checkbox.imageset/icon_grid_checkbox.pdf b/qmuidemo/Images.xcassets/icon_grid_checkbox.imageset/icon_grid_checkbox.pdf new file mode 100644 index 00000000..401a4ed6 Binary files /dev/null and b/qmuidemo/Images.xcassets/icon_grid_checkbox.imageset/icon_grid_checkbox.pdf differ diff --git a/qmuidemo/Images.xcassets/icon_grid_control.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_grid_control.imageset/Contents.json new file mode 100644 index 00000000..1b4e3bc3 --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_grid_control.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon_grid_control.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/qmuidemo/Images.xcassets/icon_grid_control.imageset/icon_grid_control.pdf b/qmuidemo/Images.xcassets/icon_grid_control.imageset/icon_grid_control.pdf new file mode 100644 index 00000000..0097192e Binary files /dev/null and b/qmuidemo/Images.xcassets/icon_grid_control.imageset/icon_grid_control.pdf differ diff --git a/qmuidemo/Images.xcassets/icon_grid_sheet.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_grid_sheet.imageset/Contents.json new file mode 100644 index 00000000..fd067662 --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_grid_sheet.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon_grid_sheet.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/qmuidemo/Images.xcassets/icon_grid_sheet.imageset/icon_grid_sheet.pdf b/qmuidemo/Images.xcassets/icon_grid_sheet.imageset/icon_grid_sheet.pdf new file mode 100644 index 00000000..4d5d291c Binary files /dev/null and b/qmuidemo/Images.xcassets/icon_grid_sheet.imageset/icon_grid_sheet.pdf differ diff --git a/qmuidemo/Images.xcassets/icon_grid_tabBar.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_grid_tabBar.imageset/Contents.json new file mode 100644 index 00000000..f6fbef16 --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_grid_tabBar.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon_grid_tabBar.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/qmuidemo/Images.xcassets/icon_grid_tabBarItem.imageset/icon_grid_tabBarItem.pdf b/qmuidemo/Images.xcassets/icon_grid_tabBar.imageset/icon_grid_tabBar.pdf similarity index 100% rename from qmuidemo/Images.xcassets/icon_grid_tabBarItem.imageset/icon_grid_tabBarItem.pdf rename to qmuidemo/Images.xcassets/icon_grid_tabBar.imageset/icon_grid_tabBar.pdf diff --git a/qmuidemo/Images.xcassets/icon_grid_theme.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_grid_theme.imageset/Contents.json new file mode 100644 index 00000000..c8d21ed5 --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_grid_theme.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icon_grid_theme.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/icon_grid_theme.imageset/icon_grid_theme.pdf b/qmuidemo/Images.xcassets/icon_grid_theme.imageset/icon_grid_theme.pdf new file mode 100644 index 00000000..66764c45 Binary files /dev/null and b/qmuidemo/Images.xcassets/icon_grid_theme.imageset/icon_grid_theme.pdf differ diff --git a/qmuidemo/Images.xcassets/launch_logo.imageset/Contents.json b/qmuidemo/Images.xcassets/launch_logo.imageset/Contents.json index 70474b53..e83801ed 100644 --- a/qmuidemo/Images.xcassets/launch_logo.imageset/Contents.json +++ b/qmuidemo/Images.xcassets/launch_logo.imageset/Contents.json @@ -1,12 +1,12 @@ { "images" : [ { - "idiom" : "universal", - "filename" : "launch_logo.pdf" + "filename" : "launch_logo.pdf", + "idiom" : "universal" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/qmuidemo/Images.xcassets/launch_logo.imageset/launch_logo.pdf b/qmuidemo/Images.xcassets/launch_logo.imageset/launch_logo.pdf index b7fed17a..5ddf45d3 100644 Binary files a/qmuidemo/Images.xcassets/launch_logo.imageset/launch_logo.pdf and b/qmuidemo/Images.xcassets/launch_logo.imageset/launch_logo.pdf differ diff --git a/qmuidemo/Images.xcassets/popover_container_arrow.imageset/Contents.json b/qmuidemo/Images.xcassets/popover_container_arrow.imageset/Contents.json new file mode 100644 index 00000000..dcdad35c --- /dev/null +++ b/qmuidemo/Images.xcassets/popover_container_arrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "popover_container_arrow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/qmuidemo/Images.xcassets/popover_container_arrow.imageset/popover_container_arrow.pdf b/qmuidemo/Images.xcassets/popover_container_arrow.imageset/popover_container_arrow.pdf new file mode 100644 index 00000000..e2374677 Binary files /dev/null and b/qmuidemo/Images.xcassets/popover_container_arrow.imageset/popover_container_arrow.pdf differ diff --git a/qmuidemo/Info.plist b/qmuidemo/Info.plist index 0d606339..eff2ac4a 100644 --- a/qmuidemo/Info.plist +++ b/qmuidemo/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 3.1.8 + $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion - 17 + $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS NSCameraUsageDescription diff --git a/qmuidemo/Modules/Common/Configuration/QDThemeManager.h b/qmuidemo/Modules/Common/Configuration/QDThemeManager.h index 5de3340c..345fa0a0 100644 --- a/qmuidemo/Modules/Common/Configuration/QDThemeManager.h +++ b/qmuidemo/Modules/Common/Configuration/QDThemeManager.h @@ -9,22 +9,41 @@ #import #import "QDThemeProtocol.h" -/// 当主题发生变化时,会发送这个通知 -extern NSString *const QDThemeChangedNotification; +/// 简单对 QMUIThemeManager 做一层业务的封装,省去类型转换的工作量 +@interface QDThemeManager : NSObject -/// 主题发生改变前的值,类型为 NSObject,可能为 NSNull -extern NSString *const QDThemeBeforeChangedName; +@property(class, nonatomic, readonly, nullable) NSObject *currentTheme; +@end + +@interface UIColor (QDTheme) -/// 主题发生改变后的值,类型为 NSObject,可能为 NSNull -extern NSString *const QDThemeAfterChangedName; +@property(class, nonatomic, strong, readonly) UIColor *qd_backgroundColor; +@property(class, nonatomic, strong, readonly) UIColor *qd_backgroundColorLighten; +@property(class, nonatomic, strong, readonly) UIColor *qd_backgroundColorHighlighted; +@property(class, nonatomic, strong, readonly) UIColor *qd_tintColor; +@property(class, nonatomic, strong, readonly) UIColor *qd_titleTextColor; +@property(class, nonatomic, strong, readonly) UIColor *qd_mainTextColor; +@property(class, nonatomic, strong, readonly) UIColor *qd_descriptionTextColor; +@property(class, nonatomic, strong, readonly) UIColor *qd_placeholderColor; +@property(class, nonatomic, strong, readonly) UIColor *qd_codeColor; +@property(class, nonatomic, strong, readonly) UIColor *qd_separatorColor; +@property(class, nonatomic, strong, readonly) UIColor *qd_gridItemTintColor; +@end -/** - * QMUI Demo 的皮肤管理器,当需要换肤时,请为 currentTheme 赋值;当需要获取当前皮肤时,可访问 currentTheme 属性。 - * 可通过监听 QDThemeChangedNotification 通知来捕获换肤事件,默认地,QDCommonViewController 及 QDCommonTableViewController 均已支持响应换肤,其响应方法是通过 QDChangingThemeDelegate 接口来实现的。 - */ -@interface QDThemeManager : NSObject +@interface UIImage (QDTheme) + +@property(class, nonatomic, strong, readonly) UIImage *qd_navigationBarBackgroundImage; +@property(class, nonatomic, strong, readonly) UIImage *qd_navigationBarBackIndicatorImage; +@property(class, nonatomic, strong, readonly) UIImage *qd_navigationBarCloseImage; +@property(class, nonatomic, strong, readonly) UIImage *qd_navigationBarDisclosureIndicatorImage; +@property(class, nonatomic, strong, readonly) UIImage *qd_tableViewCellDisclosureIndicatorImage; +@property(class, nonatomic, strong, readonly) UIImage *qd_tableViewCellCheckmarkImage; +@property(class, nonatomic, strong, readonly) UIImage *qd_tableViewCellDetailButtonImage; +@property(class, nonatomic, strong, readonly) UIImage *qd_searchBarTextFieldBackgroundImage; +@property(class, nonatomic, strong, readonly) UIImage *qd_searchBarBackgroundImage; +@end -+ (instancetype)sharedInstance; +@interface UIVisualEffect (QDTheme) -@property(nonatomic, strong) NSObject *currentTheme; +@property(class, nonatomic, strong, readonly) UIVisualEffect *qd_standardBlurEffect; @end diff --git a/qmuidemo/Modules/Common/Configuration/QDThemeManager.m b/qmuidemo/Modules/Common/Configuration/QDThemeManager.m index 5216294d..af6e0bea 100644 --- a/qmuidemo/Modules/Common/Configuration/QDThemeManager.m +++ b/qmuidemo/Modules/Common/Configuration/QDThemeManager.m @@ -7,11 +7,35 @@ // #import "QDThemeManager.h" -#import "QMUIConfigurationTemplate.h" -NSString *const QDThemeChangedNotification = @"QDThemeChangedNotification"; -NSString *const QDThemeBeforeChangedName = @"QDThemeBeforeChangedName"; -NSString *const QDThemeAfterChangedName = @"QDThemeAfterChangedName"; +@interface QDThemeManager () + +@property(nonatomic, strong) UIColor *qd_backgroundColor; +@property(nonatomic, strong) UIColor *qd_backgroundColorLighten; +@property(nonatomic, strong) UIColor *qd_backgroundColorHighlighted; +@property(nonatomic, strong) UIColor *qd_tintColor; +@property(nonatomic, strong) UIColor *qd_titleTextColor; +@property(nonatomic, strong) UIColor *qd_mainTextColor; +@property(nonatomic, strong) UIColor *qd_descriptionTextColor; +@property(nonatomic, strong) UIColor *qd_placeholderColor; +@property(nonatomic, strong) UIColor *qd_codeColor; +@property(nonatomic, strong) UIColor *qd_separatorColor; +@property(nonatomic, strong) UIColor *qd_gridItemTintColor; + +@property(nonatomic, strong) UIImage *qd_navigationBarBackgroundImage; +@property(nonatomic, strong) UIImage *qd_navigationBarBackIndicatorImage; +@property(nonatomic, strong) UIImage *qd_navigationBarCloseImage; +@property(nonatomic, strong) UIImage *qd_navigationBarDisclosureIndicatorImage; +@property(nonatomic, strong) UIImage *qd_tableViewCellDisclosureIndicatorImage; +@property(nonatomic, strong) UIImage *qd_tableViewCellCheckmarkImage; +@property(nonatomic, strong) UIImage *qd_tableViewCellDetailButtonImage; +@property(nonatomic, strong) UIImage *qd_searchBarTextFieldBackgroundImage; +@property(nonatomic, strong) UIImage *qd_searchBarBackgroundImage; + +@property(nonatomic, strong) UIVisualEffect *qd_standardBlueEffect; + +@property(class, nonatomic, strong, readonly) QDThemeManager *sharedInstance; +@end @implementation QDThemeManager @@ -30,42 +54,176 @@ + (id)allocWithZone:(struct _NSZone *)zone{ - (instancetype)init { if (self = [super init]) { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleThemeChangedNotification:) name:QDThemeChangedNotification object:nil]; + self.qd_backgroundColor = [UIColor qmui_colorWithName:@"qd_backgroundColor" themeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, NSObject *theme) { + return theme.themeBackgroundColor; + }]; + self.qd_backgroundColorLighten = [UIColor qmui_colorWithName:@"qd_backgroundColorLighten" themeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject * _Nullable theme) { + return theme.themeBackgroundColorLighten; + }]; + self.qd_backgroundColorHighlighted = [UIColor qmui_colorWithName:@"qd_backgroundColorHighlighted" themeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, NSObject *theme) { + return theme.themeBackgroundColorHighlighted; + }]; + self.qd_tintColor = [UIColor qmui_colorWithName:@"qd_tintColor" themeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, NSObject *theme) { + return theme.themeTintColor; + }]; + self.qd_titleTextColor = [UIColor qmui_colorWithName:@"qd_titleTextColor" themeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, NSObject *theme) { + return theme.themeTitleTextColor; + }]; + self.qd_mainTextColor = [UIColor qmui_colorWithName:@"qd_mainTextColor" themeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, NSObject *theme) { + return theme.themeMainTextColor; + }]; + self.qd_descriptionTextColor = [UIColor qmui_colorWithName:@"qd_descriptionTextColor" themeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, NSObject *theme) { + return theme.themeDescriptionTextColor; + }]; + self.qd_placeholderColor = [UIColor qmui_colorWithName:@"qd_placeholderColor" themeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, NSObject *theme) { + return theme.themePlaceholderColor; + }]; + self.qd_codeColor = [UIColor qmui_colorWithName:@"qd_codeColor" themeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, NSObject *theme) { + return theme.themeCodeColor; + }]; + self.qd_separatorColor = [UIColor qmui_colorWithName:@"qd_separatorColor" themeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, NSObject *theme) { + return theme.themeSeparatorColor; + }]; + self.qd_gridItemTintColor = [UIColor qmui_colorWithName:@"qd_gridItemTintColor" themeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject * _Nullable theme) { + return theme.themeGridItemTintColor; + }]; + + self.qd_navigationBarBackgroundImage = [UIImage qmui_imageWithName:@"qd_navigationBarBackgroundImage" themeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, NSObject * _Nullable theme) { + return [UIImage qmui_imageWithColor:theme.themeTintColor]; + }]; + self.qd_navigationBarBackIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavBack size:CGSizeMake(12, 20) tintColor:UIColor.whiteColor]; + self.qd_navigationBarCloseImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavClose size:CGSizeMake(16, 16) tintColor:UIColor.whiteColor]; + self.qd_navigationBarDisclosureIndicatorImage = [[UIImage qmui_imageWithShape:QMUIImageShapeTriangle size:CGSizeMake(8, 5) tintColor:nil] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + self.qd_tableViewCellDisclosureIndicatorImage = [UIImage qmui_imageWithName:@"qd_tableViewCellDisclosureIndicatorImage" themeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject * _Nullable theme) { + return [identifier isEqualToString:QDThemeIdentifierDark] ? [UIImage qmui_imageWithShape:QMUIImageShapeDisclosureIndicator size:CGSizeMake(6, 10) lineWidth:1 tintColor:UIColorMake(98, 100, 104)] : [UIImage qmui_imageWithShape:QMUIImageShapeDisclosureIndicator size:CGSizeMake(6, 10) lineWidth:1 tintColor:UIColorGray7]; + }]; + self.qd_tableViewCellCheckmarkImage = [UIImage qmui_imageWithName:@"qd_tableViewCellCheckmarkImage" themeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject * _Nullable theme) { + return [UIImage qmui_imageWithShape:QMUIImageShapeCheckmark size:CGSizeMake(15, 12) tintColor:self.qd_tintColor]; + }]; + self.qd_tableViewCellDetailButtonImage = [UIImage qmui_imageWithName:@"qd_tableViewCellDetailButtonImage" themeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject * _Nullable theme) { + return [UIImage qmui_imageWithShape:QMUIImageShapeDetailButtonImage size:CGSizeMake(20, 20) tintColor:self.qd_tintColor]; + }]; + self.qd_searchBarTextFieldBackgroundImage = [UIImage qmui_imageWithName:@"qd_searchBarTextFieldBackgroundImage" themeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return [UISearchBar qmui_generateTextFieldBackgroundImageWithColor:theme.themeBackgroundColorHighlighted]; + }]; + self.qd_searchBarBackgroundImage = [UIImage qmui_imageWithName:@"qd_searchBarBackgroundImage" themeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return [UISearchBar qmui_generateBackgroundImageWithColor:theme.themeBackgroundColor borderColor:nil]; + }]; + + self.qd_standardBlueEffect = [UIVisualEffect qmui_effectWithName:@"qd_standardBlueEffect" themeProvider:^UIVisualEffect * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject * _Nullable theme) { + return [UIBlurEffect effectWithStyle:[identifier isEqualToString:QDThemeIdentifierDark] ? UIBlurEffectStyleDark : UIBlurEffectStyleLight]; + }]; } return self; } -- (void)handleThemeChangedNotification:(NSNotification *)notification { - NSObject *themeBeforeChanged = notification.userInfo[QDThemeBeforeChangedName]; - themeBeforeChanged = [themeBeforeChanged isKindOfClass:[NSNull class]] ? nil : themeBeforeChanged; - - NSObject *themeAfterChanged = notification.userInfo[QDThemeAfterChangedName]; - themeAfterChanged = [themeAfterChanged isKindOfClass:[NSNull class]] ? nil : themeAfterChanged; - - [self themeBeforeChanged:themeBeforeChanged afterChanged:themeAfterChanged]; ++ (NSObject *)currentTheme { + return QMUIThemeManagerCenter.defaultThemeManager.currentTheme; } -- (void)setCurrentTheme:(NSObject *)currentTheme { - BOOL isThemeChanged = _currentTheme != currentTheme; - NSObject *themeBeforeChanged = nil; - if (isThemeChanged) { - themeBeforeChanged = _currentTheme; - } - _currentTheme = currentTheme; - if (isThemeChanged && themeBeforeChanged) {// 从 nil 变成某个 theme 就不发通知了,初始化时会自动 apply,这里只需要处理在 QD 里手动更改 theme 的场景就行 - [currentTheme applyConfigurationTemplate]; - [[NSNotificationCenter defaultCenter] postNotificationName:QDThemeChangedNotification object:self userInfo:@{QDThemeBeforeChangedName: themeBeforeChanged ?: [NSNull null], QDThemeAfterChangedName: currentTheme ?: [NSNull null]}]; - } +@end + +@implementation UIColor (QDTheme) + ++ (instancetype)qd_sharedInstance { + static dispatch_once_t onceToken; + static UIColor *instance = nil; + dispatch_once(&onceToken,^{ + instance = [[super allocWithZone:NULL] init]; + }); + return instance; +} + ++ (UIColor *)qd_backgroundColor { + return QDThemeManager.sharedInstance.qd_backgroundColor; +} + ++ (UIColor *)qd_backgroundColorLighten { + return QDThemeManager.sharedInstance.qd_backgroundColorLighten; +} + ++ (UIColor *)qd_backgroundColorHighlighted { + return QDThemeManager.sharedInstance.qd_backgroundColorHighlighted; +} + ++ (UIColor *)qd_tintColor { + return QDThemeManager.sharedInstance.qd_tintColor; +} + ++ (UIColor *)qd_titleTextColor { + return QDThemeManager.sharedInstance.qd_titleTextColor; +} + ++ (UIColor *)qd_mainTextColor { + return QDThemeManager.sharedInstance.qd_mainTextColor; +} + ++ (UIColor *)qd_descriptionTextColor { + return QDThemeManager.sharedInstance.qd_descriptionTextColor; +} + ++ (UIColor *)qd_placeholderColor { + return QDThemeManager.sharedInstance.qd_placeholderColor; +} + ++ (UIColor *)qd_codeColor { + return QDThemeManager.sharedInstance.qd_codeColor; +} + ++ (UIColor *)qd_separatorColor { + return QDThemeManager.sharedInstance.qd_separatorColor; +} + ++ (UIColor *)qd_gridItemTintColor { + return QDThemeManager.sharedInstance.qd_gridItemTintColor; +} + +@end + +@implementation UIImage (QDTheme) + ++ (UIImage *)qd_navigationBarBackgroundImage { + return QDThemeManager.sharedInstance.qd_navigationBarBackgroundImage; +} + ++ (UIImage *)qd_navigationBarBackIndicatorImage { + return QDThemeManager.sharedInstance.qd_navigationBarBackIndicatorImage; +} + ++ (UIImage *)qd_navigationBarCloseImage { + return QDThemeManager.sharedInstance.qd_navigationBarCloseImage; +} + ++ (UIImage *)qd_navigationBarDisclosureIndicatorImage { + return QDThemeManager.sharedInstance.qd_navigationBarDisclosureIndicatorImage; +} + ++ (UIImage *)qd_tableViewCellDisclosureIndicatorImage { + return QDThemeManager.sharedInstance.qd_tableViewCellDisclosureIndicatorImage; +} + ++ (UIImage *)qd_tableViewCellCheckmarkImage { + return QDThemeManager.sharedInstance.qd_tableViewCellCheckmarkImage; } -#pragma mark - ++ (UIImage *)qd_tableViewCellDetailButtonImage { + return QDThemeManager.sharedInstance.qd_tableViewCellDetailButtonImage; +} + ++ (UIImage *)qd_searchBarTextFieldBackgroundImage { + return QDThemeManager.sharedInstance.qd_searchBarTextFieldBackgroundImage; +} + ++ (UIImage *)qd_searchBarBackgroundImage { + return QDThemeManager.sharedInstance.qd_searchBarBackgroundImage; +} + +@end + +@implementation UIVisualEffect (QDTheme) -- (void)themeBeforeChanged:(NSObject *)themeBeforeChanged afterChanged:(NSObject *)themeAfterChanged { - // 主题发生变化,在这里更新全局 UI 控件的 appearance - [QDCommonUI renderGlobalAppearances]; - - // 更新表情 icon 的颜色 - [QDUIHelper updateEmotionImages]; ++ (UIVisualEffect *)qd_standardBlurEffect { + return QDThemeManager.sharedInstance.qd_standardBlueEffect; } @end diff --git a/qmuidemo/Modules/Common/Configuration/QDThemeProtocol.h b/qmuidemo/Modules/Common/Configuration/QDThemeProtocol.h index e9e54edb..a636faf6 100644 --- a/qmuidemo/Modules/Common/Configuration/QDThemeProtocol.h +++ b/qmuidemo/Modules/Common/Configuration/QDThemeProtocol.h @@ -13,21 +13,39 @@ @required +/// 界面背景色 +- (UIColor *)themeBackgroundColor; + +/// 浅一点的界面背景色,例如 Grouped 类型的列表的 cell 背景 +- (UIColor *)themeBackgroundColorLighten; + +/// 在通用背景色上的 item 点击高亮背景色,例如 cell 的 highlightedBackgroundColor +- (UIColor *)themeBackgroundColorHighlighted; + +/// 主题色 - (UIColor *)themeTintColor; -- (UIColor *)themeListTextColor; -- (UIColor *)themeCodeColor; -- (UIColor *)themeGridItemTintColor; -- (NSString *)themeName; +/// 最深的文字颜色,可用于标题或者输入框文字 +- (UIColor *)themeTitleTextColor; -@end +/// 主要内容的文字颜色,例如列表的 textLabel +- (UIColor *)themeMainTextColor; +/// 界面上一些附属说明的小字颜色 +- (UIColor *)themeDescriptionTextColor; -/// 所有能响应主题变化的对象均应实现这个协议,目前主要用于 QDCommonViewController 及 QDCommonTableViewController -@protocol QDChangingThemeDelegate +/// 输入框 placeholder 的颜色 +- (UIColor *)themePlaceholderColor; -@required +/// 文字中的代码颜色 +- (UIColor *)themeCodeColor; + +/// 分隔线颜色,例如 tableViewSeparator +- (UIColor *)themeSeparatorColor; -- (void)themeBeforeChanged:(NSObject *)themeBeforeChanged afterChanged:(NSObject *)themeAfterChanged; +/// App 首页每个单元格的颜色 +- (UIColor *)themeGridItemTintColor; + +- (NSString *)themeName; @end diff --git a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplate.m b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplate.m index 18a50ca8..e2d4da30 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplate.m +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplate.m @@ -7,7 +7,6 @@ // #import "QMUIConfigurationTemplate.h" -#import @implementation QMUIConfigurationTemplate @@ -23,27 +22,32 @@ - (void)applyConfigurationTemplate { QMUICMI.whiteColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:1]; // UIColorWhite : 白色(不用 [UIColor whiteColor] 是希望保持颜色空间为 RGB) QMUICMI.blackColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:1]; // UIColorBlack : 黑色(不用 [UIColor blackColor] 是希望保持颜色空间为 RGB) QMUICMI.grayColor = UIColorGray4; // UIColorGray : 最常用的灰色 - QMUICMI.grayDarkenColor = UIColorGray3; // UIColorGrayDarken : 深一点的灰色 + QMUICMI.grayDarkenColor = UIColor.qd_mainTextColor; // UIColorGrayDarken : 深一点的灰色 QMUICMI.grayLightenColor = UIColorGray7; // UIColorGrayLighten : 浅一点的灰色 QMUICMI.redColor = UIColorMake(250, 58, 58); // UIColorRed : 红色 QMUICMI.greenColor = UIColorTheme4; // UIColorGreen : 绿色 - QMUICMI.blueColor = UIColorMake(49, 189, 243); // UIColorBlue : 蓝色 + QMUICMI.blueColor = UIColorTheme6; // UIColorBlue : 蓝色 QMUICMI.yellowColor = UIColorTheme3; // UIColorYellow : 黄色 QMUICMI.linkColor = UIColorMake(56, 116, 171); // UIColorLink : 文字链接颜色 QMUICMI.disabledColor = UIColorGray; // UIColorDisabled : 全局 disabled 的颜色,一般用于 UIControl 等控件 - QMUICMI.backgroundColor = UIColorWhite; // UIColorForBackground : 界面背景色,默认用于 QMUICommonViewController.view 的背景色 + QMUICMI.backgroundColor = UIColor.qd_backgroundColor; // UIColorForBackground : 界面背景色,默认用于 QMUICommonViewController.view 的背景色 QMUICMI.maskDarkColor = UIColorMakeWithRGBA(0, 0, 0, .35f); // UIColorMask : 深色的背景遮罩,默认用于 QMAlertController、QMUIDialogViewController 等弹出控件的遮罩 QMUICMI.maskLightColor = UIColorMakeWithRGBA(255, 255, 255, .5f); // UIColorMaskWhite : 浅色的背景遮罩,QMUIKit 里默认没用到,只是占个位 - QMUICMI.separatorColor = UIColorMake(222, 224, 226); // UIColorSeparator : 全局默认的分割线颜色,默认用于列表分隔线颜色、UIView (QMUI_Border) 分隔线颜色 + QMUICMI.separatorColor = UIColor.qd_separatorColor; // UIColorSeparator : 全局默认的分割线颜色,默认用于列表分隔线颜色、UIView (QMUIBorder) 分隔线颜色 QMUICMI.separatorDashedColor = UIColorMake(17, 17, 17); // UIColorSeparatorDashed : 全局默认的虚线分隔线的颜色,默认 QMUIKit 暂时没用到 - QMUICMI.placeholderColor = UIColorGray8; // UIColorPlaceholder,全局的输入框的 placeholder 颜色,默认用于 QMUITextField、QMUITextView,不影响系统 UIKit 的输入框 + QMUICMI.placeholderColor = UIColor.qd_placeholderColor; // UIColorPlaceholder,全局的输入框的 placeholder 颜色,默认用于 QMUITextField、QMUITextView,不影响系统 UIKit 的输入框 // 测试用的颜色 QMUICMI.testColorRed = UIColorMakeWithRGBA(255, 0, 0, .3); QMUICMI.testColorGreen = UIColorMakeWithRGBA(0, 255, 0, .3); QMUICMI.testColorBlue = UIColorMakeWithRGBA(0, 0, 255, .3); +#pragma mark - QMUILog + QMUICMI.shouldPrintDefaultLog = YES; // ShouldPrintDefaultLog : 是否允许输出 QMUILogLevelDefault 级别的 log + QMUICMI.shouldPrintInfoLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelInfo 级别的 log + QMUICMI.shouldPrintWarnLog = YES; // ShouldPrintWarnLog : 是否允许输出 QMUILogLevelWarn 级别的 log + QMUICMI.shouldPrintQMUIWarnLogToConsole = YES; // ShouldPrintQMUIWarnLogToConsole : 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上 #pragma mark - UIControl @@ -53,109 +57,136 @@ - (void)applyConfigurationTemplate { #pragma mark - UIButton QMUICMI.buttonHighlightedAlpha = UIControlHighlightedAlpha; // ButtonHighlightedAlpha : QMUIButton 在 highlighted 时的 alpha,不影响系统的 UIButton QMUICMI.buttonDisabledAlpha = UIControlDisabledAlpha; // ButtonDisabledAlpha : QMUIButton 在 disabled 时的 alpha,不影响系统的 UIButton - QMUICMI.buttonTintColor = self.themeTintColor; // ButtonTintColor : QMUIButton 默认的 tintColor,不影响系统的 UIButton - - QMUICMI.ghostButtonColorBlue = UIColorBlue; // GhostButtonColorBlue : QMUIGhostButtonColorBlue 的颜色 - QMUICMI.ghostButtonColorRed = UIColorRed; // GhostButtonColorRed : QMUIGhostButtonColorRed 的颜色 - QMUICMI.ghostButtonColorGreen = UIColorGreen; // GhostButtonColorGreen : QMUIGhostButtonColorGreen 的颜色 - QMUICMI.ghostButtonColorGray = UIColorGray; // GhostButtonColorGray : QMUIGhostButtonColorGray 的颜色 - QMUICMI.ghostButtonColorWhite = UIColorWhite; // GhostButtonColorWhite : QMUIGhostButtonColorWhite 的颜色 - - QMUICMI.fillButtonColorBlue = UIColorBlue; // FillButtonColorBlue : QMUIFillButtonColorBlue 的颜色 - QMUICMI.fillButtonColorRed = UIColorRed; // FillButtonColorRed : QMUIFillButtonColorRed 的颜色 - QMUICMI.fillButtonColorGreen = UIColorGreen; // FillButtonColorGreen : QMUIFillButtonColorGreen 的颜色 - QMUICMI.fillButtonColorGray = UIColorGray; // FillButtonColorGray : QMUIFillButtonColorGray 的颜色 - QMUICMI.fillButtonColorWhite = UIColorWhite; // FillButtonColorWhite : QMUIFillButtonColorWhite 的颜色 + QMUICMI.buttonTintColor = UIColor.qd_tintColor; // ButtonTintColor : QMUIButton 默认的 tintColor,不影响系统的 UIButton -#pragma mark - TextField & TextView - QMUICMI.textFieldTintColor = self.themeTintColor; // TextFieldTintColor : QMUITextField、QMUITextView 的 tintColor,不影响 UIKit 的输入框 +#pragma mark - TextInput + QMUICMI.textFieldTextColor = UIColor.qd_titleTextColor; // TextFieldTextColor : QMUITextField、QMUITextView 的 textColor,不影响 UIKit 的输入框 + QMUICMI.textFieldTintColor = UIColor.qd_tintColor; // TextFieldTintColor : QMUITextField、QMUITextView 的 tintColor,不影响 UIKit 的输入框 QMUICMI.textFieldTextInsets = UIEdgeInsetsMake(0, 7, 0, 7); // TextFieldTextInsets : QMUITextField 的内边距,不影响 UITextField + QMUICMI.keyboardAppearance = UIKeyboardAppearanceDefault; // KeyboardAppearance : UITextView、UITextField、UISearchBar 的 keyboardAppearance + +#pragma mark - UISwitch + QMUICMI.switchOnTintColor = UIColor.qd_tintColor; // SwitchOnTintColor : UISwitch 打开时的背景色(除了圆点外的其他颜色) + QMUICMI.switchOffTintColor = UIColor.qd_separatorColor; // SwitchOffTintColor : UISwitch 关闭时的背景色(除了圆点外的其他颜色) + QMUICMI.switchThumbTintColor = nil; // SwitchThumbTintColor : UISwitch 中间的操控圆点的颜色 #pragma mark - NavigationBar + if (@available(iOS 15.0, *)) { + QMUICMI.navBarUsesStandardAppearanceOnly = YES; // NavBarUsesStandardAppearanceOnly : 对于 iOS 15 的系统,UINavigationBar 的样式分为滚动前和滚动后,虽然系统的注释里说了如果没设置 scrollEdgeAppearance 则会用 standardAppearance 代替,但实际运行效果是 scrollEdgeAppearance 默认并不会保持与 standardAppearance 一致,所以这里提供一个开关,允许你在打开开关时让 QMUI 帮你同步 standardAppearance 的值,以使 App 保持与 iOS 14 相同的效果。如需打开该开关,请保证在其他 NavBar 开关之前设置。 + } + QMUICMI.navBarContainerClasses = @[QMUINavigationController.class]; // NavBarContainerClasses : NavigationBar 系列开关被用于 UIAppearance 时的生效范围(默认情况下除了用于 UIAppearance 外,还用于实现了 QMUINavigationControllerAppearanceDelegate 的 UIViewController),默认为 nil。当赋值为 nil 或者空数组时等效于 @[UINavigationController.class],也即对所有 UINavigationBar 生效,包括系统的通讯录(ContactsUI.framework)、打印等。当值不为空时,获取 UINavigationBar 的 appearance 请使用 UINavigationBar.qmui_appearanceConfigured 方法代替系统的 UINavigationBar.appearance。请保证这个配置项先于其他任意 NavBar 配置项执行。 QMUICMI.navBarHighlightedAlpha = 0.2f; // NavBarHighlightedAlpha : QMUINavigationButton 在 highlighted 时的 alpha QMUICMI.navBarDisabledAlpha = 0.2f; // NavBarDisabledAlpha : QMUINavigationButton 在 disabled 时的 alpha - QMUICMI.navBarButtonFont = UIFontMake(17); // NavBarButtonFont : QMUINavigationButtonTypeNormal 的字体(由于系统存在一些 bug,这个属性默认不对 UIBarButtonItem 生效) - QMUICMI.navBarButtonFontBold = UIFontBoldMake(17); // NavBarButtonFontBold : QMUINavigationButtonTypeBold 的字体 - QMUICMI.navBarBackgroundImage = [UIImageMake(@"navigationbar_background") resizableImageWithCapInsets:UIEdgeInsetsMake(0, 1, 0, 1) resizingMode:UIImageResizingModeStretch]; // NavBarBackgroundImage : UINavigationBar 的背景图,注意 navigationBar 的高度会受多个因素(是否全面屏、是否使用了 navigationItem.prompt、是否将 UISearchBar 作为 titleView)的影响,要检查各种情况是否都显示正常。 - QMUICMI.navBarShadowImage = [UIImage qmui_imageWithColor:UIColorClear size:CGSizeMake(4, PixelOne) cornerRadius:0]; // NavBarShadowImage : UINavigationBar.shadowImage,也即导航栏底部那条分隔线 + QMUICMI.navBarButtonFont = UIFontMake(17); // NavBarButtonFont : UINavigationBar 里 UIBarButtonItem 以及 QMUINavigationButtonTypeNormal 的字体 + QMUICMI.navBarButtonFontBold = UIFontBoldMake(17); // NavBarButtonFontBold : iOS 15 及以后用于设置 UINavigationBar 里 Done 类型的 UIBarButtonItem 以及 QMUINavigationButtonTypeBold 的字体,iOS 14 及以前只对后者生效 + QMUICMI.navBarBackgroundImage = UIImage.qd_navigationBarBackgroundImage; // NavBarBackgroundImage : UINavigationBar 的背景图,注意 navigationBar 的高度会受多个因素(是否全面屏、是否使用了 navigationItem.prompt、是否将 UISearchBar 作为 titleView)的影响,要检查各种情况是否都显示正常。 + if (@available(iOS 15.0, *)) { + QMUICMI.navBarRemoveBackgroundEffectAutomatically = YES; // NavBarRemoveBackgroundEffectAutomatically : iOS 15 及以后,QMUI 里的 UINavigationBar 使用的是 UINavigationBarAppearance 来设置样式,新方式默认是 backgroundImage 和 backgroundEffect 共存的,而 iOS 14 及以前的旧方式,一旦设置了 backgroundImage 则 backgroundEffect 自动会被移除,因此提供该开关允许业务将行为回退到 iOS 14 及以前的效果。默认为 NO。 + } + QMUICMI.navBarShadowImage = nil; // NavBarShadowImage : UINavigationBar.shadowImage,也即导航栏底部那条分隔线,配合 NavBarShadowImageColor 使用。 + QMUICMI.navBarShadowImageColor = UIColorClear; // NavBarShadowImageColor : UINavigationBar.shadowImage 的颜色,如果为 nil,则使用 NavBarShadowImage 的值,如果 NavBarShadowImage 也为 nil,则使用系统默认的分隔线。如果不为 nil,而 NavBarShadowImage 为 nil,则自动创建一张 1px 高的图并将其设置为 NavBarShadowImageColor 的颜色然后设置上去,如果 NavBarShadowImage 不为 nil 且 renderingMode 不为 UIImageRenderingModeAlwaysOriginal,则将 NavBarShadowImage 设置为 NavBarShadowImageColor 的颜色然后设置上去。 QMUICMI.navBarBarTintColor = nil; // NavBarBarTintColor : UINavigationBar.barTintColor,也即背景色 - QMUICMI.navBarTintColor = UIColorWhite; // NavBarTintColor : QMUINavigationBar 的 tintColor,也即导航栏上面的按钮颜色 + QMUICMI.navBarStyle = UIBarStyleDefault; // NavBarStyle : UINavigationBar 的 barStyle + QMUICMI.navBarTintColor = UIColorWhite; // NavBarTintColor : NavBarContainerClasses 里的 UINavigationBar 的 tintColor,也即导航栏上面的按钮颜色 QMUICMI.navBarTitleColor = NavBarTintColor; // NavBarTitleColor : UINavigationBar 的标题颜色,以及 QMUINavigationTitleView 的默认文字颜色 QMUICMI.navBarTitleFont = UIFontBoldMake(17); // NavBarTitleFont : UINavigationBar 的标题字体,以及 QMUINavigationTitleView 的默认字体 - QMUICMI.navBarLargeTitleColor = nil; // NavBarLargeTitleColor : UINavigationBar 在大标题模式下的标题颜色,仅在 iOS 11 之后才有效 - QMUICMI.navBarLargeTitleFont = nil; // NavBarLargeTitleFont : UINavigationBar 在大标题模式下的标题字体,仅在 iOS 11 之后才有效 + QMUICMI.navBarLargeTitleColor = nil; // NavBarLargeTitleColor : UINavigationBar 在大标题模式下的标题颜色 + QMUICMI.navBarLargeTitleFont = nil; // NavBarLargeTitleFont : UINavigationBar 在大标题模式下的标题字体 QMUICMI.navBarBackButtonTitlePositionAdjustment = UIOffsetZero; // NavBarBarBackButtonTitlePositionAdjustment : 导航栏返回按钮的文字偏移 - QMUICMI.sizeNavBarBackIndicatorImageAutomatically = YES; // SizeNavBarBackIndicatorImageAutomatically : 是否要自动调整 NavBarBackIndicatorImage 的 size 为 (13, 21) - QMUICMI.navBarBackIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavBack size:CGSizeMake(12, 20) tintColor:NavBarTintColor]; // NavBarBackIndicatorImage : 导航栏的返回按钮的图片,图片尺寸建议为(13, 21),否则最终的图片位置无法与系统原生的位置保持一致 - QMUICMI.navBarCloseButtonImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavClose size:CGSizeMake(16, 16) tintColor:NavBarTintColor]; // NavBarCloseButtonImage : QMUINavigationButton 用到的 × 的按钮图片 + QMUICMI.sizeNavBarBackIndicatorImageAutomatically = NO; // SizeNavBarBackIndicatorImageAutomatically : 是否要自动调整 NavBarBackIndicatorImage 的 size 为 (13, 21) + QMUICMI.navBarBackIndicatorImage = UIImage.qd_navigationBarBackIndicatorImage; // NavBarBackIndicatorImage : 导航栏的返回按钮的图片,图片尺寸建议为(13, 21),否则最终的图片位置无法与系统原生的位置保持一致 + QMUICMI.navBarCloseButtonImage = UIImage.qd_navigationBarCloseImage; // NavBarCloseButtonImage : QMUINavigationButton 用到的 × 的按钮图片 QMUICMI.navBarLoadingMarginRight = 3; // NavBarLoadingMarginRight : QMUINavigationTitleView 里左边 loading 的右边距 QMUICMI.navBarAccessoryViewMarginLeft = 5; // NavBarAccessoryViewMarginLeft : QMUINavigationTitleView 里右边 accessoryView 的左边距 QMUICMI.navBarActivityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;// NavBarActivityIndicatorViewStyle : QMUINavigationTitleView 里左边 loading 的主题 - QMUICMI.navBarAccessoryViewTypeDisclosureIndicatorImage = [[UIImage qmui_imageWithShape:QMUIImageShapeTriangle size:CGSizeMake(8, 5) tintColor:nil] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; // NavBarAccessoryViewTypeDisclosureIndicatorImage : QMUINavigationTitleView 右边箭头的图片 + QMUICMI.navBarAccessoryViewTypeDisclosureIndicatorImage = UIImage.qd_navigationBarDisclosureIndicatorImage; // NavBarAccessoryViewTypeDisclosureIndicatorImage : QMUINavigationTitleView 右边箭头的图片 + #pragma mark - TabBar - QMUICMI.tabBarBackgroundImage = [[UIImage qmui_imageWithColor:UIColorMake(249, 249, 249)] resizableImageWithCapInsets:UIEdgeInsetsMake(1, 1, 1, 1)]; // TabBarBackgroundImage : UITabBar 的背景图,建议使用 resizableImage,否则在 UITabBar (NavigationController) 的 setBackgroundImage: 里会每次都视为 image 发生了变化(isEqual: 为 NO) - QMUICMI.tabBarBarTintColor = nil; // TabBarBarTintColor : UITabBar 的 barTintColor + if (@available(iOS 15.0, *)) { + QMUICMI.tabBarUsesStandardAppearanceOnly = YES; // TabBarUsesStandardAppearanceOnly : 对于 iOS 15 的系统,UITabBar 的样式分为滚动前和滚动后,虽然系统的注释里说了如果没设置 scrollEdgeAppearance 则会用 standardAppearance 代替,但实际运行效果是 scrollEdgeAppearance 默认并不会保持与 standardAppearance 一致,所以这里提供一个开关,允许你在打开开关时让 QMUI 帮你同步 standardAppearance 的值,以使 App 保持与 iOS 14 相同的效果。如需打开该开关,请保证在其他 NavBar 开关之前设置。 + } + QMUICMI.tabBarContainerClasses = nil; // TabBarContainerClasses : TabBar 系列开关的生效范围,默认为 nil,当赋值为 nil 或者空数组时等效于 @[UITabBarController.class],也即对所有 UITabBar 生效。当值不为空时,获取 UITabBar 的 appearance 请使用 UITabBar.qmui_appearanceConfigured 方法代替系统的 UITabBar.appearance。请保证这个配置项先于其他任意 TabBar 配置项执行。 + QMUICMI.tabBarBackgroundImage = nil; // TabBarBackgroundImage : UITabBar 的背景图,建议使用 resizableImage,否则在 UITabBar (NavigationController) 的 setBackgroundImage: 里会每次都视为 image 发生了变化(isEqual: 为 NO) + if (@available(iOS 15.0, *)) { + QMUICMI.tabBarRemoveBackgroundEffectAutomatically = YES; // TabBarRemoveBackgroundEffectAutomatically : iOS 15 及以后,QMUI 里的 UITabBar 使用的是 UITabBarAppearance 来设置样式,新方式默认是 backgroundImage 和 backgroundEffect 共存的,而 iOS 14 及以前的旧方式,一旦设置了 backgroundImage 则 backgroundEffect 自动会被移除,因此提供该开关允许业务将行为回退到 iOS 14 及以前的效果。默认为 NO。 + } + QMUICMI.tabBarBarTintColor = nil; // TabBarBarTintColor : UITabBar 的 barTintColor,如果需要看到磨砂效果则应该提供半透明的色值 QMUICMI.tabBarShadowImageColor = UIColorSeparator; // TabBarShadowImageColor : UITabBar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片 - QMUICMI.tabBarTintColor = UIColorMake(4, 189, 231); // TabBarTintColor : UITabBar 的 tintColor - QMUICMI.tabBarItemTitleColor = UIColorGray6; // TabBarItemTitleColor : 未选中的 UITabBarItem 的标题颜色 - QMUICMI.tabBarItemTitleColorSelected = TabBarTintColor; // TabBarItemTitleColorSelected : 选中的 UITabBarItem 的标题颜色 - QMUICMI.tabBarItemTitleFont = nil; // TabBarItemTitleFont : UITabBarItem 的标题字体 + QMUICMI.tabBarStyle = UIBarStyleDefault; // TabBarStyle : UITabBar 的 barStyle + QMUICMI.tabBarItemTitleFont = UIFontMake(10); // TabBarItemTitleFont : UITabBarItem 的标题字体 + QMUICMI.tabBarItemTitleFontSelected = nil; // TabBarItemTitleFontSelected : 选中的 UITabBarItem 的标题字体 + QMUICMI.tabBarItemTitleColor = UIColor.qd_descriptionTextColor; // TabBarItemTitleColor : 未选中的 UITabBarItem 的标题颜色 + QMUICMI.tabBarItemTitleColorSelected = UIColor.qd_tintColor; // TabBarItemTitleColorSelected : 选中的 UITabBarItem 的标题颜色 + QMUICMI.tabBarItemImageColor = TabBarItemTitleColor; // TabBarItemImageColor : UITabBarItem 未选中时的图片颜色(该配置项在 iOS 12 及以下的系统对 QMUIThemeImage 无效,请自行在 provider 内处理颜色。注意非选中状态的图片需要指定为 UIImageRenderingModeAlwaysOriginal,系统限制如此) + QMUICMI.tabBarItemImageColorSelected = TabBarItemTitleColorSelected; // TabBarItemImageColorSelected : UITabBarItem 选中时的图片颜色 + #pragma mark - Toolbar + if (@available(iOS 15.0, *)) { + QMUICMI.toolBarUsesStandardAppearanceOnly = YES; // ToolBarUsesStandardAppearanceOnly : 对于 iOS 15 的系统,UIToolbar 的样式分为滚动前和滚动后,虽然系统的注释里说了如果没设置 scrollEdgeAppearance 则会用 standardAppearance 代替,但实际运行效果是 scrollEdgeAppearance 默认并不会保持与 standardAppearance 一致,所以这里提供一个开关,允许你在打开开关时让 QMUI 帮你同步 standardAppearance 的值,以使 App 保持与 iOS 14 相同的效果。如需打开该开关,请保证在其他 NavBar 开关之前设置。 + } + QMUICMI.toolBarContainerClasses = @[QMUINavigationController.class]; // ToolBarContainerClasses : ToolBar 系列开关的生效范围,默认为 nil,当赋值为 nil 或者空数组时等效于 @[UINavigationController.class],也即对所有 UIToolbar 生效。当值不为空时,获取 UIToolbar 的 appearance 请使用 UIToolbar.qmui_appearanceConfigured 方法代替系统的 UIToolbar.appearance。请保证这个配置项先于其他任意 ToolBar 配置项执行。 QMUICMI.toolBarHighlightedAlpha = 0.4f; // ToolBarHighlightedAlpha : QMUIToolbarButton 在 highlighted 状态下的 alpha QMUICMI.toolBarDisabledAlpha = 0.4f; // ToolBarDisabledAlpha : QMUIToolbarButton 在 disabled 状态下的 alpha - QMUICMI.toolBarTintColor = UIColorBlue; // ToolBarTintColor : UIToolbar 的 tintColor,以及 QMUIToolbarButton normal 状态下的文字颜色 + QMUICMI.toolBarTintColor = UIColor.qd_tintColor; // ToolBarTintColor : NavBarContainerClasses 里的 UIToolbar 的 tintColor,以及 QMUIToolbarButton normal 状态下的文字颜色 QMUICMI.toolBarTintColorHighlighted = [ToolBarTintColor colorWithAlphaComponent:ToolBarHighlightedAlpha]; // ToolBarTintColorHighlighted : QMUIToolbarButton 在 highlighted 状态下的文字颜色 QMUICMI.toolBarTintColorDisabled = [ToolBarTintColor colorWithAlphaComponent:ToolBarDisabledAlpha]; // ToolBarTintColorDisabled : QMUIToolbarButton 在 disabled 状态下的文字颜色 - QMUICMI.toolBarBackgroundImage = nil; // ToolBarBackgroundImage : UIToolbar 的背景图 - QMUICMI.toolBarBarTintColor = nil; // ToolBarBarTintColor : UIToolbar 的 tintColor - QMUICMI.toolBarShadowImageColor = UIColorSeparator; // ToolBarShadowImageColor : UIToolbar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片 + QMUICMI.toolBarBackgroundImage = nil; // ToolBarBackgroundImage : NavBarContainerClasses 里的 UIToolbar 的背景图 + if (@available(iOS 15.0, *)) { + QMUICMI.toolBarRemoveBackgroundEffectAutomatically = YES; // ToolBarRemoveBackgroundEffectAutomatically : iOS 15 及以后,QMUI 里的 UIToolbar 使用的是 UIToolbarAppearance 来设置样式,新方式默认是 backgroundImage 和 backgroundEffect 共存的,而 iOS 14 及以前的旧方式,一旦设置了 backgroundImage 则 backgroundEffect 自动会被移除,因此提供该开关允许业务将行为回退到 iOS 14 及以前的效果。默认为 NO。 + } + QMUICMI.toolBarBarTintColor = nil; // ToolBarBarTintColor : NavBarContainerClasses 里的 UIToolbar 的 tintColor + QMUICMI.toolBarShadowImageColor = nil; // ToolBarShadowImageColor : NavBarContainerClasses 里的 UIToolbar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片 + QMUICMI.toolBarStyle = UIBarStyleDefault; // ToolBarStyle : NavBarContainerClasses 里的 UIToolbar 的 barStyle QMUICMI.toolBarButtonFont = UIFontMake(17); // ToolBarButtonFont : QMUIToolbarButton 的字体 #pragma mark - SearchBar - QMUICMI.searchBarTextFieldBackground = UIColorMake(237, 238, 240); // SearchBarTextFieldBackground : QMUISearchBar 里的文本框的背景颜色 + QMUICMI.searchBarTextFieldBackgroundImage = UIImage.qd_searchBarTextFieldBackgroundImage; // SearchBarTextFieldBackgroundImage : QMUISearchBar 里的文本框的背景图,图片高度会决定输入框的高度 QMUICMI.searchBarTextFieldBorderColor = nil; // SearchBarTextFieldBorderColor : QMUISearchBar 里的文本框的边框颜色 - QMUICMI.searchBarBottomBorderColor = UIColorClear; // SearchBarBottomBorderColor : QMUISearchBar 底部分隔线颜色 - QMUICMI.searchBarBarTintColor = UIColorWhite; // SearchBarBarTintColor : QMUISearchBar 的 barTintColor,也即背景色 - QMUICMI.searchBarTintColor = self.themeTintColor; // SearchBarTintColor : QMUISearchBar 的 tintColor,也即上面的操作控件的主题色 - QMUICMI.searchBarTextColor = UIColorBlack; // SearchBarTextColor : QMUISearchBar 里的文本框的文字颜色 - QMUICMI.searchBarPlaceholderColor = UIColorMake(136, 136, 143); // SearchBarPlaceholderColor : QMUISearchBar 里的文本框的 placeholder 颜色 - QMUICMI.searchBarFont = nil; // SearchBarFont : QMUISearchBar 里的文本框的文字字体及 placeholder 的字体 + QMUICMI.searchBarTextFieldCornerRadius = 4.0; // SearchBarTextFieldCornerRadius : QMUISearchBar 里的文本框的圆角大小,-1 表示圆角大小为输入框高度的一半 + QMUICMI.searchBarBackgroundImage = UIImage.qd_searchBarBackgroundImage; // SearchBarBackgroundImage : 搜索框的背景图,如果需要设置底部分隔线的颜色也请绘制到图片里 + QMUICMI.searchBarTintColor = UIColor.qd_tintColor; // SearchBarTintColor : QMUISearchBar 的 tintColor,也即上面的操作控件的主题色 + QMUICMI.searchBarTextColor = UIColor.qd_titleTextColor; // SearchBarTextColor : QMUISearchBar 里的文本框的文字颜色 + QMUICMI.searchBarPlaceholderColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject *theme) { + if ([identifier isEqualToString:QDThemeIdentifierDark]) { + return theme.themePlaceholderColor; + } + return UIColorMake(136, 136, 143); + }]; // SearchBarPlaceholderColor : QMUISearchBar 里的文本框的 placeholder 颜色 + QMUICMI.searchBarFont = UIFontMake(15); // SearchBarFont : QMUISearchBar 里的文本框的文字字体及 placeholder 的字体 QMUICMI.searchBarSearchIconImage = nil; // SearchBarSearchIconImage : QMUISearchBar 里的放大镜 icon QMUICMI.searchBarClearIconImage = nil; // SearchBarClearIconImage : QMUISearchBar 里的文本框输入文字时右边的清空按钮的图片 - QMUICMI.searchBarTextFieldCornerRadius = 4.0; // SearchBarTextFieldCornerRadius : QMUISearchBar 里的文本框的圆角大小,-1 表示圆角大小为输入框高度的一半 -#pragma mark - TableView / TableViewCell +#pragma mark - Plain TableView QMUICMI.tableViewEstimatedHeightEnabled = YES; // TableViewEstimatedHeightEnabled : 是否要开启全局 QMUITableView 和 UITableView 的 estimatedRow(Section/Footer)Height - QMUICMI.tableViewBackgroundColor = nil; // TableViewBackgroundColor : Plain 类型的 QMUITableView 的背景色颜色 - QMUICMI.tableViewGroupedBackgroundColor = UIColorMake(246, 246, 246); // TableViewGroupedBackgroundColor : Grouped 类型的 QMUITableView 的背景色 + QMUICMI.tableViewBackgroundColor = UIColorForBackground; // TableViewBackgroundColor : Plain 类型的 QMUITableView 的背景色颜色 QMUICMI.tableSectionIndexColor = UIColorGrayDarken; // TableSectionIndexColor : 列表右边的字母索引条的文字颜色 QMUICMI.tableSectionIndexBackgroundColor = UIColorClear; // TableSectionIndexBackgroundColor : 列表右边的字母索引条的背景色 QMUICMI.tableSectionIndexTrackingBackgroundColor = UIColorClear; // TableSectionIndexTrackingBackgroundColor : 列表右边的字母索引条在选中时的背景色 QMUICMI.tableViewSeparatorColor = UIColorSeparator; // TableViewSeparatorColor : 列表的分隔线颜色 QMUICMI.tableViewCellNormalHeight = 56; // TableViewCellNormalHeight : QMUITableView 的默认 cell 高度 - QMUICMI.tableViewCellTitleLabelColor = UIColorGray3; // TableViewCellTitleLabelColor : QMUITableViewCell 的 textLabel 的文字颜色 - QMUICMI.tableViewCellDetailLabelColor = UIColorGray5; // TableViewCellDetailLabelColor : QMUITableViewCell 的 detailTextLabel 的文字颜色 - QMUICMI.tableViewCellBackgroundColor = UIColorWhite; // TableViewCellBackgroundColor : QMUITableViewCell 的背景色 - QMUICMI.tableViewCellSelectedBackgroundColor = UIColorMake(238, 239, 241); // TableViewCellSelectedBackgroundColor : QMUITableViewCell 点击时的背景色 + QMUICMI.tableViewCellTitleLabelColor = UIColor.qd_mainTextColor; // TableViewCellTitleLabelColor : QMUITableViewCell 的 textLabel 的文字颜色 + QMUICMI.tableViewCellDetailLabelColor = UIColor.qd_descriptionTextColor; // TableViewCellDetailLabelColor : QMUITableViewCell 的 detailTextLabel 的文字颜色 + QMUICMI.tableViewCellBackgroundColor = UIColorForBackground; // TableViewCellBackgroundColor : QMUITableViewCell 的背景色 + QMUICMI.tableViewCellSelectedBackgroundColor = UIColor.qd_backgroundColorHighlighted; // TableViewCellSelectedBackgroundColor : QMUITableViewCell 点击时的背景色 QMUICMI.tableViewCellWarningBackgroundColor = UIColorYellow; // TableViewCellWarningBackgroundColor : QMUITableViewCell 用于表示警告时的背景色,备用 - QMUICMI.tableViewCellDisclosureIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeDisclosureIndicator size:CGSizeMake(6, 10) lineWidth:1 tintColor:UIColorMake(173, 180, 190)]; // TableViewCellDisclosureIndicatorImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryDisclosureIndicator 时的箭头的图片 - QMUICMI.tableViewCellCheckmarkImage = [UIImage qmui_imageWithShape:QMUIImageShapeCheckmark size:CGSizeMake(15, 12) tintColor:self.themeTintColor]; // TableViewCellCheckmarkImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryCheckmark 时的打钩的图片 - QMUICMI.tableViewCellDetailButtonImage = [UIImage qmui_imageWithShape:QMUIImageShapeDetailButtonImage size:CGSizeMake(20, 20) tintColor:self.themeTintColor]; // TableViewCellDetailButtonImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryDetailButton 或 UITableViewCellAccessoryDetailDisclosureButton 时右边的 i 按钮图片 + QMUICMI.tableViewCellDisclosureIndicatorImage = UIImage.qd_tableViewCellDisclosureIndicatorImage; // TableViewCellDisclosureIndicatorImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryDisclosureIndicator 时的箭头的图片 + QMUICMI.tableViewCellCheckmarkImage = UIImage.qd_tableViewCellCheckmarkImage; // TableViewCellCheckmarkImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryCheckmark 时的打钩的图片 + QMUICMI.tableViewCellDetailButtonImage = UIImage.qd_tableViewCellDetailButtonImage; // TableViewCellDetailButtonImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryDetailButton 或 UITableViewCellAccessoryDetailDisclosureButton 时右边的 i 按钮图片 QMUICMI.tableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator = 12; // TableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator : 列表 cell 右边的 i 按钮和向右箭头之间的间距(仅当两者都使用了自定义图片并且同时显示时才生效) - QMUICMI.tableViewSectionHeaderBackgroundColor = UIColorMake(244, 244, 244); // TableViewSectionHeaderBackgroundColor : Plain 类型的 QMUITableView sectionHeader 的背景色 - QMUICMI.tableViewSectionFooterBackgroundColor = UIColorMake(244, 244, 244); // TableViewSectionFooterBackgroundColor : Plain 类型的 QMUITableView sectionFooter 的背景色 + QMUICMI.tableViewSectionHeaderBackgroundColor = UIColor.qd_separatorColor; // TableViewSectionHeaderBackgroundColor : Plain 类型的 QMUITableView sectionHeader 的背景色 + QMUICMI.tableViewSectionFooterBackgroundColor = UIColor.qd_separatorColor; // TableViewSectionFooterBackgroundColor : Plain 类型的 QMUITableView sectionFooter 的背景色 QMUICMI.tableViewSectionHeaderFont = UIFontBoldMake(12); // TableViewSectionHeaderFont : Plain 类型的 QMUITableView sectionHeader 里的文字字体 QMUICMI.tableViewSectionFooterFont = UIFontBoldMake(12); // TableViewSectionFooterFont : Plain 类型的 QMUITableView sectionFooter 里的文字字体 QMUICMI.tableViewSectionHeaderTextColor = UIColorGray5; // TableViewSectionHeaderTextColor : Plain 类型的 QMUITableView sectionHeader 里的文字颜色 @@ -164,86 +195,158 @@ - (void)applyConfigurationTemplate { QMUICMI.tableViewSectionFooterAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewSectionFooterAccessoryMargins : Plain 类型的 QMUITableView sectionFooter accessoryView 的间距 QMUICMI.tableViewSectionHeaderContentInset = UIEdgeInsetsMake(4, 15, 4, 15); // TableViewSectionHeaderContentInset : Plain 类型的 QMUITableView sectionHeader 里的内容的 padding QMUICMI.tableViewSectionFooterContentInset = UIEdgeInsetsMake(4, 15, 4, 15); // TableViewSectionFooterContentInset : Plain 类型的 QMUITableView sectionFooter 里的内容的 padding + if (@available(iOS 15, *)) { + QMUICMI.tableViewSectionHeaderTopPadding = 0; // TableViewSectionHeaderTopPadding : Plain 类型的 QMUITableView 在 iOS 15 上的 sectionHeaderTopPadding 值,仅当存在 sectionHeader 时才有效,系统的默认值为 UITableViewAutomaticDimension,表现出来是22pt的空隙 + } + +#pragma mark - Grouped TableView + + QMUICMI.tableViewGroupedBackgroundColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject * _Nullable theme) { + if ([identifier isEqualToString:QDThemeIdentifierDark]) { + return QMUICMI.tableViewBackgroundColor; + } + return UIColorMake(246, 246, 246); + }]; // TableViewGroupedBackgroundColor : Grouped 类型的 QMUITableView 的背景色 + QMUICMI.tableViewGroupedSeparatorColor = TableViewSeparatorColor; // TableViewGroupedSeparatorColor : Grouped 类型的 QMUITableView 分隔线颜色 + QMUICMI.tableViewGroupedCellTitleLabelColor = TableViewCellTitleLabelColor; // TableViewGroupedCellTitleLabelColor : Grouped 类型的 QMUITableView cell 里的标题颜色 + QMUICMI.tableViewGroupedCellDetailLabelColor = TableViewCellDetailLabelColor; // TableViewGroupedCellDetailLabelColor : Grouped 类型的 QMUITableView cell 里的副标题颜色 + QMUICMI.tableViewGroupedCellBackgroundColor = UIColor.qd_backgroundColorLighten; // TableViewGroupedCellBackgroundColor : Grouped 类型的 QMUITableView cell 背景色 + QMUICMI.tableViewGroupedCellSelectedBackgroundColor = TableViewCellSelectedBackgroundColor; // TableViewGroupedCellSelectedBackgroundColor : Grouped 类型的 QMUITableView cell 点击时的背景色 + QMUICMI.tableViewGroupedCellWarningBackgroundColor = TableViewCellWarningBackgroundColor; // tableViewGroupedCellWarningBackgroundColor : Grouped 类型的 QMUITableView cell 在提醒状态下的背景色 QMUICMI.tableViewGroupedSectionHeaderFont = UIFontMake(12); // TableViewGroupedSectionHeaderFont : Grouped 类型的 QMUITableView sectionHeader 里的文字字体 QMUICMI.tableViewGroupedSectionFooterFont = UIFontMake(12); // TableViewGroupedSectionFooterFont : Grouped 类型的 QMUITableView sectionFooter 里的文字字体 QMUICMI.tableViewGroupedSectionHeaderTextColor = UIColorGrayDarken; // TableViewGroupedSectionHeaderTextColor : Grouped 类型的 QMUITableView sectionHeader 里的文字颜色 - QMUICMI.tableViewGroupedSectionFooterTextColor = UIColorGray; // TableViewGroupedSectionFooterTextColor : Grouped 类型的 QMUITableView sectionFooter 里的文字颜色 - QMUICMI.tableViewGroupedSectionHeaderAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewGroupedSectionHeaderAccessoryMargins : Grouped 类型的 QMUITableView sectionHeader accessoryView 的间距 - QMUICMI.tableViewGroupedSectionFooterAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewGroupedSectionFooterAccessoryMargins : Grouped 类型的 QMUITableView sectionFooter accessoryView 的间距 - QMUICMI.tableViewGroupedSectionHeaderDefaultHeight = UITableViewAutomaticDimension; // TableViewGroupedSectionHeaderDefaultHeight : Grouped 类型的 QMUITableView sectionHeader 的默认高度(也即没使用自定义的 sectionHeaderView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN - QMUICMI.tableViewGroupedSectionFooterDefaultHeight = UITableViewAutomaticDimension; // TableViewGroupedSectionFooterDefaultHeight : Grouped 类型的 QMUITableView sectionFooter 的默认高度(也即没使用自定义的 sectionFooterView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN - QMUICMI.tableViewGroupedSectionHeaderContentInset = UIEdgeInsetsMake(16, PreferredValueForiPhone(20, 15, 15, 15), 8, PreferredValueForiPhone(20, 15, 15, 15)); // TableViewGroupedSectionHeaderContentInset : Grouped 类型的 QMUITableView sectionHeader 里的内容的 padding - QMUICMI.tableViewGroupedSectionFooterContentInset = UIEdgeInsetsMake(8, 15, 2, 15); // TableViewGroupedSectionFooterContentInset : Grouped 类型的 QMUITableView sectionFooter 里的内容的 padding + QMUICMI.tableViewGroupedSectionFooterTextColor = TableViewGroupedSectionHeaderTextColor; // TableViewGroupedSectionFooterTextColor : Grouped 类型的 QMUITableView sectionFooter 里的文字颜色 + QMUICMI.tableViewGroupedSectionHeaderAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewGroupedSectionHeaderAccessoryMargins : Grouped 类型的 QMUITableView sectionHeader accessoryView 的间距 + QMUICMI.tableViewGroupedSectionFooterAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewGroupedSectionFooterAccessoryMargins : Grouped 类型的 QMUITableView sectionFooter accessoryView 的间距 + QMUICMI.tableViewGroupedSectionHeaderDefaultHeight = 20; // TableViewGroupedSectionHeaderDefaultHeight : Grouped 类型的 QMUITableView sectionHeader 的默认高度(也即没使用自定义的 sectionHeaderView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN + QMUICMI.tableViewGroupedSectionFooterDefaultHeight = 0; // TableViewGroupedSectionFooterDefaultHeight : Grouped 类型的 QMUITableView sectionFooter 的默认高度(也即没使用自定义的 sectionFooterView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN + QMUICMI.tableViewGroupedSectionHeaderContentInset = UIEdgeInsetsMake(16, PreferredValueForVisualDevice(20, 15), 8, PreferredValueForVisualDevice(20, 15)); // TableViewGroupedSectionHeaderContentInset : Grouped 类型的 QMUITableView sectionHeader 里的内容的 padding + QMUICMI.tableViewGroupedSectionFooterContentInset = UIEdgeInsetsMake(8, TableViewGroupedSectionHeaderContentInset.left, 2, TableViewGroupedSectionHeaderContentInset.right); // TableViewGroupedSectionFooterContentInset : Grouped 类型的 QMUITableView sectionFooter 里的内容的 padding + if (@available(iOS 15, *)) { + QMUICMI.tableViewGroupedSectionHeaderTopPadding = 0; // TableViewGroupedSectionHeaderTopPadding : Grouped 类型的 QMUITableView 在 iOS 15 上的 sectionHeaderTopPadding 值,仅当存在 sectionHeader 时才有效,系统的默认值为 UITableViewAutomaticDimension,表现出来是0。 + } + +#pragma mark - InsetGrouped TableView + QMUICMI.tableViewInsetGroupedCornerRadius = 10; // TableViewInsetGroupedCornerRadius : InsetGrouped 类型的 UITableView 内 cell 的圆角值 + QMUICMI.tableViewInsetGroupedHorizontalInset = PreferredValueForVisualDevice(20, 15); // TableViewInsetGroupedHorizontalInset: InsetGrouped 类型的 UITableView 内的左右缩进值 + QMUICMI.tableViewInsetGroupedBackgroundColor = TableViewGroupedBackgroundColor; // TableViewInsetGroupedBackgroundColor : InsetGrouped 类型的 UITableView 的背景色 + QMUICMI.tableViewInsetGroupedSeparatorColor = TableViewGroupedSeparatorColor; // TableViewInsetGroupedSeparatorColor : InsetGrouped 类型的 QMUITableView 分隔线颜色 + QMUICMI.tableViewInsetGroupedCellTitleLabelColor = TableViewGroupedCellTitleLabelColor; // TableViewInsetGroupedCellTitleLabelColor : InsetGrouped 类型的 QMUITableView cell 里的标题颜色 + QMUICMI.tableViewInsetGroupedCellDetailLabelColor = TableViewGroupedCellDetailLabelColor; // TableViewInsetGroupedCellDetailLabelColor : InsetGrouped 类型的 QMUITableView cell 里的副标题颜色 + QMUICMI.tableViewInsetGroupedCellBackgroundColor = TableViewGroupedCellBackgroundColor; // TableViewInsetGroupedCellBackgroundColor : InsetGrouped 类型的 QMUITableView cell 背景色 + QMUICMI.tableViewInsetGroupedCellSelectedBackgroundColor = TableViewGroupedCellSelectedBackgroundColor; // TableViewInsetGroupedCellSelectedBackgroundColor : InsetGrouped 类型的 QMUITableView cell 点击时的背景色 + QMUICMI.tableViewInsetGroupedCellWarningBackgroundColor = TableViewGroupedCellWarningBackgroundColor; // TableViewInsetGroupedCellWarningBackgroundColor : InsetGrouped 类型的 QMUITableView cell 在提醒状态下的背景色 + QMUICMI.tableViewInsetGroupedSectionHeaderFont = TableViewGroupedSectionHeaderFont; // TableViewInsetGroupedSectionHeaderFont : InsetGrouped 类型的 QMUITableView sectionHeader 里的文字字体 + QMUICMI.tableViewInsetGroupedSectionFooterFont = TableViewGroupedSectionFooterFont; // TableViewInsetGroupedSectionFooterFont : InsetGrouped 类型的 QMUITableView sectionFooter 里的文字字体 + QMUICMI.tableViewInsetGroupedSectionHeaderTextColor = TableViewGroupedSectionHeaderTextColor; // TableViewInsetGroupedSectionHeaderTextColor : InsetGrouped 类型的 QMUITableView sectionHeader 里的文字颜色 + QMUICMI.tableViewInsetGroupedSectionFooterTextColor = TableViewGroupedSectionFooterTextColor; // TableViewInsetGroupedSectionFooterTextColor : InsetGrouped 类型的 QMUITableView sectionFooter 里的文字颜色 + QMUICMI.tableViewInsetGroupedSectionHeaderAccessoryMargins = TableViewGroupedSectionHeaderAccessoryMargins; // TableViewInsetGroupedSectionHeaderAccessoryMargins : InsetGrouped 类型的 QMUITableView sectionHeader accessoryView 的间距 + QMUICMI.tableViewInsetGroupedSectionFooterAccessoryMargins = TableViewGroupedSectionFooterAccessoryMargins; // TableViewInsetGroupedSectionFooterAccessoryMargins : InsetGrouped 类型的 QMUITableView sectionFooter accessoryView 的间距 + QMUICMI.tableViewInsetGroupedSectionHeaderDefaultHeight = TableViewGroupedSectionHeaderDefaultHeight; // TableViewInsetGroupedSectionHeaderDefaultHeight : InsetGrouped 类型的 QMUITableView sectionHeader 的默认高度(也即没使用自定义的 sectionHeaderView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN + QMUICMI.tableViewInsetGroupedSectionFooterDefaultHeight = TableViewGroupedSectionFooterDefaultHeight; // TableViewInsetGroupedSectionFooterDefaultHeight : InsetGrouped 类型的 QMUITableView sectionFooter 的默认高度(也即没使用自定义的 sectionFooterView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN + QMUICMI.tableViewInsetGroupedSectionHeaderContentInset = TableViewGroupedSectionHeaderContentInset; // TableViewInsetGroupedSectionHeaderContentInset : InsetGrouped 类型的 QMUITableView sectionHeader 里的内容的 padding + QMUICMI.tableViewInsetGroupedSectionFooterContentInset = TableViewGroupedSectionFooterContentInset; // TableViewInsetGroupedSectionFooterContentInset : InsetGrouped 类型的 QMUITableView sectionFooter 里的内容的 padding + if (@available(iOS 15, *)) { + QMUICMI.tableViewInsetGroupedSectionHeaderTopPadding = 0; // TableViewInsetGroupedSectionHeaderTopPadding : InsetGrouped 类型的 QMUITableView 在 iOS 15 上的 sectionHeaderTopPadding 值,仅当存在 sectionHeader 时才有效,系统的默认值为 UITableViewAutomaticDimension,表现出来是0。 + } #pragma mark - UIWindowLevel QMUICMI.windowLevelQMUIAlertView = UIWindowLevelAlert - 4.0; // UIWindowLevelQMUIAlertView : QMUIModalPresentationViewController、QMUIPopupContainerView 里使用的 UIWindow 的 windowLevel - -#pragma mark - QMUILog - QMUICMI.shouldPrintDefaultLog = YES; // ShouldPrintDefaultLog : 是否允许输出 QMUILogLevelDefault 级别的 log - QMUICMI.shouldPrintInfoLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelInfo 级别的 log - QMUICMI.shouldPrintWarnLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelWarn 级别的 log + QMUICMI.windowLevelQMUIConsole = 1; // UIWindowLevelQMUIConsole : QMUIConsole 内部的 UIWindow 的 windowLevel #pragma mark - QMUIBadge - QMUICMI.badgeBackgroundColor = UIColorRed; // BadgeBackgroundColor : UIBarButtoItem、UITabBarItem 上的未读数的背景色 - QMUICMI.badgeTextColor = UIColorWhite; // BadgeTextColor : UIBarButtoItem、UITabBarItem 上的未读数的文字颜色 - QMUICMI.badgeFont = UIFontBoldMake(11); // BadgeFont : UIBarButtoItem、UITabBarItem 上的未读数的字体 - QMUICMI.badgeContentEdgeInsets = UIEdgeInsetsMake(2, 4, 2, 4); // BadgeContentEdgeInsets : UIBarButtoItem、UITabBarItem 上的未读数与圆圈之间的 padding - QMUICMI.badgeCenterOffset = CGPointMake(14, -10); // BadgeCenterOffset : UIBarButtoItem、UITabBarItem 上的未读数相对于 item 中心的偏移 - QMUICMI.badgeCenterOffsetLandscape = CGPointMake(16, -7); // BadgeCenterOffsetLandscape : UIBarButtoItem、UITabBarItem 上的未读数在横屏下相对于 item 中心的偏移 + QMUICMI.badgeBackgroundColor = UIColorRed; // BadgeBackgroundColor : QMUIBadge 上的未读数的背景色 + QMUICMI.badgeTextColor = UIColorWhite; // BadgeTextColor : QMUIBadge 上的未读数的文字颜色 + QMUICMI.badgeFont = UIFontBoldMake(11); // BadgeFont : QMUIBadge 上的未读数的字体 + QMUICMI.badgeContentEdgeInsets = UIEdgeInsetsMake(2, 4, 2, 4); // BadgeContentEdgeInsets : QMUIBadge 上的未读数与圆圈之间的 padding + QMUICMI.badgeOffset = CGPointMake(-9, 11); // BadgeOffset : QMUIBadge 上的未读数相对于目标 view 右上角的偏移 + QMUICMI.badgeOffsetLandscape = CGPointMake(-9, 6); // BadgeOffsetLandscape : QMUIBadge 上的未读数在横屏下相对于目标 view 右上角的偏移 - QMUICMI.updatesIndicatorColor = UIColorRed; // UpdatesIndicatorColor : UIBarButtoItem、UITabBarItem 上的未读红点的颜色 - QMUICMI.updatesIndicatorSize = CGSizeMake(7, 7); // UpdatesIndicatorSize : UIBarButtoItem、UITabBarItem 上的未读红点的大小 - QMUICMI.updatesIndicatorCenterOffset = CGPointMake(14, -10); // UpdatesIndicatorCenterOffset : UIBarButtoItem、UITabBarItem 上的未读红点相对于 item 中心的偏移 - QMUICMI.updatesIndicatorCenterOffsetLandscape = CGPointMake(14, -10); // UpdatesIndicatorCenterOffsetLandscape : UIBarButtoItem、UITabBarItem 上的未读红点在横屏下相对于 item 中心的偏移 + QMUICMI.updatesIndicatorColor = UIColorRed; // UpdatesIndicatorColor : QMUIBadge 上的未读红点的颜色 + QMUICMI.updatesIndicatorSize = CGSizeMake(7, 7); // UpdatesIndicatorSize : QMUIBadge 上的未读红点的大小 + QMUICMI.updatesIndicatorOffset = CGPointMake(4, UpdatesIndicatorSize.height);// UpdatesIndicatorOffset : QMUIBadge 未读红点相对于目标 view 右上角的偏移 + QMUICMI.updatesIndicatorOffsetLandscape = UpdatesIndicatorOffset; // UpdatesIndicatorOffsetLandscape : QMUIBadge 未读红点在横屏下相对于目标 view 右上角的偏移 #pragma mark - Others QMUICMI.automaticCustomNavigationBarTransitionStyle = NO; // AutomaticCustomNavigationBarTransitionStyle : 界面 push/pop 时是否要自动根据两个界面的 barTintColor/backgroundImage/shadowImage 的样式差异来决定是否使用自定义的导航栏效果 QMUICMI.supportedOrientationMask = UIInterfaceOrientationMaskAll; // SupportedOrientationMask : 默认支持的横竖屏方向 - QMUICMI.automaticallyRotateDeviceOrientation = YES; // AutomaticallyRotateDeviceOrientation : 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕 - QMUICMI.statusbarStyleLightInitially = YES; // StatusbarStyleLightInitially : 默认的状态栏内容是否使用白色,默认为 NO,也即黑色 + QMUICMI.automaticallyRotateDeviceOrientation = YES; // AutomaticallyRotateDeviceOrientation : 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕(仅 iOS 15 及以前版本需要,iOS 16 系统会自动处理,该开关无意义。) + QMUICMI.defaultStatusBarStyle = UIStatusBarStyleLightContent; // DefaultStatusBarStyle : 默认的状态栏样式,默认值为 UIStatusBarStyleDefault,也即在 iOS 12 及以前是黑色文字,iOS 13 及以后会自动根据当前 App 是否处于 Dark Mode 切换颜色。如果你希望固定为白色,请设置为 UIStatusBarStyleLightContent,固定黑色则设置为 UIStatusBarStyleDarkContent。 QMUICMI.needsBackBarButtonItemTitle = NO; // NeedsBackBarButtonItemTitle : 全局是否需要返回按钮的 title,不需要则只显示一个返回image QMUICMI.hidesBottomBarWhenPushedInitially = YES; // HidesBottomBarWhenPushedInitially : QMUICommonViewController.hidesBottomBarWhenPushed 的初始值,默认为 NO,以保持与系统默认值一致,但通常建议改为 YES,因为一般只有 tabBar 首页那几个界面要求为 NO QMUICMI.preventConcurrentNavigationControllerTransitions = YES; // PreventConcurrentNavigationControllerTransitions : 自动保护 QMUINavigationController 在上一次 push/pop 尚未结束的时候就进行下一次 push/pop 的行为,避免产生 crash QMUICMI.navigationBarHiddenInitially = NO; // NavigationBarHiddenInitially : QMUINavigationControllerDelegate preferredNavigationBarHidden 的初始值,默认为NO - QMUICMI.shouldFixTabBarTransitionBugInIPhoneX = YES; // ShouldFixTabBarTransitionBugInIPhoneX : 是否需要自动修复 iOS 11 下,iPhone X 的设备在 push 界面时,tabBar 会瞬间往上跳的 bug - QMUICMI.shouldFixTabBarButtonBugForAll = NO; // ShouldFixTabBarButtonBugForAll : 是否要对 iOS 12.1.1 及以后的版本也修复手势返回时 tabBarButton 布局错误的 bug(issue #410),默认为 NO - QMUICMI.shouldPrintQMUIWarnLogToConsole = YES; // ShouldPrintQMUIWarnLogToConsole : 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上 - QMUICMI.sendAnalyticsToQMUITeam = YES; // SendAnalyticsToQMUITeam : 是否允许在 DEBUG 模式下上报 Bundle Identifier 和 Display Name 给 QMUI 统计用 + QMUICMI.shouldFixTabBarSafeAreaInsetsBug = YES; // ShouldFixTabBarSafeAreaInsetsBug : 是否要对 iOS 11 及以后的版本修复当存在 UITabBar 时,UIScrollView 的 inset.bottom 可能错误的 bug(issue #218 #934),默认为 YES + QMUICMI.shouldFixSearchBarMaskViewLayoutBug = YES; // ShouldFixSearchBarMaskViewLayoutBug : 是否自动修复 UISearchController.searchBar 被当作 tableHeaderView 使用时可能出现的布局 bug(issue #950) + QMUICMI.dynamicPreferredValueForIPad = NO; // DynamicPreferredValueForIPad : 当 iPad 处于 Slide Over 或 Split View 分屏模式下,宏 `PreferredValueForXXX` 是否把 iPad 视为某种屏幕宽度近似的 iPhone 来取值。 + QMUICMI.ignoreKVCAccessProhibited = NO; // IgnoreKVCAccessProhibited : 是否全局忽略 iOS 13 对 KVC 访问 UIKit 私有属性的限制 + QMUICMI.adjustScrollIndicatorInsetsByContentInsetAdjustment = YES; // AdjustScrollIndicatorInsetsByContentInsetAdjustment : 当将 UIScrollView.contentInsetAdjustmentBehavior 设为 UIScrollViewContentInsetAdjustmentNever 时,是否自动将 UIScrollView.automaticallyAdjustsScrollIndicatorInsets 设为 NO,以保证原本在 iOS 12 下的代码不用修改就能在 iOS 13 下正常控制滚动条的位置。 } // QMUI 2.3.0 版本里,配置表新增这个方法,返回 YES 表示在 App 启动时要自动应用这份配置表。仅当你的 App 里存在多份配置表时,才需要把除默认配置表之外的其他配置表的返回值改为 NO。 - (BOOL)shouldApplyTemplateAutomatically { - NSString *selectedThemeClassName = [[NSUserDefaults standardUserDefaults] stringForKey:QDSelectedThemeClassName]; - BOOL result = [selectedThemeClassName isEqualToString:NSStringFromClass(self.class)] || (!selectedThemeClassName && ![QDThemeManager sharedInstance].currentTheme); + [QMUIThemeManagerCenter.defaultThemeManager addThemeIdentifier:self.themeName theme:self]; + + NSString *selectedThemeIdentifier = [[NSUserDefaults standardUserDefaults] stringForKey:QDSelectedThemeIdentifier]; + BOOL result = [selectedThemeIdentifier isEqualToString:self.themeName] || (!selectedThemeIdentifier && !QMUIThemeManagerCenter.defaultThemeManager.currentThemeIdentifier); if (result) { - [QDThemeManager sharedInstance].currentTheme = self; + QMUIThemeManagerCenter.defaultThemeManager.currentTheme = self; } return result; } #pragma mark - +- (UIColor *)themeBackgroundColor { + return UIColorWhite; +} + +- (UIColor *)themeBackgroundColorLighten { + return self.themeBackgroundColor; +} + +- (UIColor *)themeBackgroundColorHighlighted { + return UIColorMake(238, 239, 241); +} + - (UIColor *)themeTintColor { - return UIColorBlue; + return UIColorTheme6; } -- (UIColor *)themeListTextColor { - return self.themeTintColor; +- (UIColor *)themeTitleTextColor { + return UIColorGray1; +} + +- (UIColor *)themeMainTextColor { + return UIColorGray3; +} + +- (UIColor *)themeDescriptionTextColor { + return UIColorGray5; +} + +- (UIColor *)themePlaceholderColor { + return UIColorGray8; } - (UIColor *)themeCodeColor { return self.themeTintColor; } +- (UIColor *)themeSeparatorColor { + return UIColorMake(222, 224, 226); +} + - (UIColor *)themeGridItemTintColor { - return nil; + return self.themeTintColor; } - (NSString *)themeName { - return @"Default"; + return QDThemeIdentifierDefault; } @end diff --git a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateDark.h b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateDark.h new file mode 100644 index 00000000..63ca9024 --- /dev/null +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateDark.h @@ -0,0 +1,12 @@ +// +// QMUIConfigurationTemplate.h +// +// Created by QMUI Team on 15/3/29. +// Copyright (c) 2015年 QMUI Team. All rights reserved. +// + +#import "QMUIConfigurationTemplate.h" + +@interface QMUIConfigurationTemplateDark : QMUIConfigurationTemplate + +@end diff --git a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateDark.m b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateDark.m new file mode 100644 index 00000000..9e033fc5 --- /dev/null +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateDark.m @@ -0,0 +1,93 @@ +// +// QMUIConfigurationTemplate.m +// qmui +// +// Created by QMUI Team on 15/3/29. +// Copyright (c) 2015年 QMUI Team. All rights reserved. +// + +#import "QMUIConfigurationTemplateDark.h" + +@implementation QMUIConfigurationTemplateDark + +#pragma mark - + +- (void)applyConfigurationTemplate { + [super applyConfigurationTemplate]; + + QMUICMI.keyboardAppearance = UIKeyboardAppearanceDark; + + QMUICMI.navBarBackgroundImage = nil; + QMUICMI.navBarStyle = UIBarStyleBlack; + + QMUICMI.tabBarBackgroundImage = nil; + QMUICMI.tabBarShadowImageColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, NSObject *theme) { + return theme.themeSeparatorColor; + }];// 这是一个 QMUIThemeColor,在应用配置表时,QMUIThemeImage 里还没 hook qmui_imageWithColor 方法,所以无法生成一个 QMUIThemeImage 对象,所以不会自动变化,所以需要手动在 Dark 配置表里再设置一次 + QMUICMI.tabBarStyle = UIBarStyleBlack; + + QMUICMI.toolBarShadowImageColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, NSObject *theme) { + return theme.themeSeparatorColor; + }];// 这是一个 QMUIThemeColor,在应用配置表时,QMUIThemeImage 里还没 hook qmui_imageWithColor 方法,所以无法生成一个 QMUIThemeImage 对象,所以不会自动变化,所以需要手动在 Dark 配置表里再设置一次 + QMUICMI.toolBarStyle = UIBarStyleBlack; +} + +// QMUI 2.3.0 版本里,配置表新增这个方法,返回 YES 表示在 App 启动时要自动应用这份配置表。仅当你的 App 里存在多份配置表时,才需要把除默认配置表之外的其他配置表的返回值改为 NO。 +- (BOOL)shouldApplyTemplateAutomatically { + [QMUIThemeManagerCenter.defaultThemeManager addThemeIdentifier:self.themeName theme:self]; + + NSString *selectedThemeIdentifier = [[NSUserDefaults standardUserDefaults] stringForKey:QDSelectedThemeIdentifier]; + BOOL result = [selectedThemeIdentifier isEqualToString:self.themeName]; + if (result) { + QMUIThemeManagerCenter.defaultThemeManager.currentTheme = self; + } + return result; +} + +#pragma mark - + +- (UIColor *)themeBackgroundColor { + return UIColorBlack; +} + +- (UIColor *)themeBackgroundColorLighten { + return UIColorMake(28, 28, 30); +} + +- (UIColor *)themeBackgroundColorHighlighted { + return UIColorMake(48, 49, 51); +} + +- (UIColor *)themeTintColor { + return UIColorTheme10; +} + +- (UIColor *)themeTitleTextColor { + return UIColorDarkGray1; +} + +- (UIColor *)themeMainTextColor { + return UIColorDarkGray3; +} + +- (UIColor *)themeDescriptionTextColor { + return UIColorDarkGray6; +} + +- (UIColor *)themePlaceholderColor { + return UIColorDarkGray8; +} + +- (UIColor *)themeCodeColor { + return self.themeTintColor; +} + +- (UIColor *)themeSeparatorColor { + return UIColorMake(46, 50, 54); +} + +- (NSString *)themeName { + return QDThemeIdentifierDark; +} + +@end diff --git a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrapefruit.h b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrapefruit.h index 7780ee27..70b73cdd 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrapefruit.h +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrapefruit.h @@ -5,17 +5,8 @@ // Copyright (c) 2015年 QMUI Team. All rights reserved. // -#import -#import "QDThemeProtocol.h" +#import "QMUIConfigurationTemplate.h" -/** - * QMUIConfigurationTemplate 是一份配置表,用于配合 QMUIConfiguration 来管理整个 App 的全局样式,使用方式: - * 在 QMUI 项目代码的文件夹里找到 QMUIConfigurationTemplate 目录,把里面所有文件复制到自己项目里,保证能被编译到即可,不需要在某些地方 import,也不需要手动运行。 - * - * @warning 更新 QMUIKit 的版本时,请留意 Release Log 里是否有提醒更新配置表,请尽量保持自己项目里的配置表与 QMUIKit 里的配置表一致,避免遗漏新的属性。 - * @warning 配置表的 class 名必须以 QMUIConfigurationTemplate 开头,并且实现 ,因为这两者是 QMUI 识别该 NSObject 是否为一份配置表的条件。 - * @warning QMUI 2.3.0 之后,配置表改为自动运行,不需要再在某个地方手动运行了。 - */ -@interface QMUIConfigurationTemplateGrapefruit : NSObject +@interface QMUIConfigurationTemplateGrapefruit : QMUIConfigurationTemplate @end diff --git a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrapefruit.m b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrapefruit.m index ea049a02..9356b92f 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrapefruit.m +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrapefruit.m @@ -7,218 +7,19 @@ // #import "QMUIConfigurationTemplateGrapefruit.h" -#import @implementation QMUIConfigurationTemplateGrapefruit #pragma mark - -- (void)applyConfigurationTemplate { - - // === 修改配置值 === // - -#pragma mark - Global Color - - QMUICMI.clearColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0]; // UIColorClear : 透明色 - QMUICMI.whiteColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:1]; // UIColorWhite : 白色(不用 [UIColor whiteColor] 是希望保持颜色空间为 RGB) - QMUICMI.blackColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:1]; // UIColorBlack : 黑色(不用 [UIColor blackColor] 是希望保持颜色空间为 RGB) - QMUICMI.grayColor = UIColorGray4; // UIColorGray : 最常用的灰色 - QMUICMI.grayDarkenColor = UIColorGray3; // UIColorGrayDarken : 深一点的灰色 - QMUICMI.grayLightenColor = UIColorGray7; // UIColorGrayLighten : 浅一点的灰色 - QMUICMI.redColor = UIColorMake(250, 58, 58); // UIColorRed : 红色 - QMUICMI.greenColor = UIColorTheme4; // UIColorGreen : 绿色 - QMUICMI.blueColor = UIColorMake(49, 189, 243); // UIColorBlue : 蓝色 - QMUICMI.yellowColor = UIColorTheme3; // UIColorYellow : 黄色 - - QMUICMI.linkColor = UIColorMake(56, 116, 171); // UIColorLink : 文字链接颜色 - QMUICMI.disabledColor = UIColorGray; // UIColorDisabled : 全局 disabled 的颜色,一般用于 UIControl 等控件 - QMUICMI.backgroundColor = UIColorWhite; // UIColorForBackground : 界面背景色,默认用于 QMUICommonViewController.view 的背景色 - QMUICMI.maskDarkColor = UIColorMakeWithRGBA(0, 0, 0, .35f); // UIColorMask : 深色的背景遮罩,默认用于 QMAlertController、QMUIDialogViewController 等弹出控件的遮罩 - QMUICMI.maskLightColor = UIColorMakeWithRGBA(255, 255, 255, .5f); // UIColorMaskWhite : 浅色的背景遮罩,QMUIKit 里默认没用到,只是占个位 - QMUICMI.separatorColor = UIColorMake(222, 224, 226); // UIColorSeparator : 全局默认的分割线颜色,默认用于列表分隔线颜色、UIView (QMUI_Border) 分隔线颜色 - QMUICMI.separatorDashedColor = UIColorMake(17, 17, 17); // UIColorSeparatorDashed : 全局默认的虚线分隔线的颜色,默认 QMUIKit 暂时没用到 - QMUICMI.placeholderColor = UIColorGray8; // UIColorPlaceholder,全局的输入框的 placeholder 颜色,默认用于 QMUITextField、QMUITextView,不影响系统 UIKit 的输入框 - - // 测试用的颜色 - QMUICMI.testColorRed = UIColorMakeWithRGBA(255, 0, 0, .3); - QMUICMI.testColorGreen = UIColorMakeWithRGBA(0, 255, 0, .3); - QMUICMI.testColorBlue = UIColorMakeWithRGBA(0, 0, 255, .3); - - -#pragma mark - UIControl - - QMUICMI.controlHighlightedAlpha = 0.5f; // UIControlHighlightedAlpha : UIControl 系列控件在 highlighted 时的 alpha,默认用于 QMUIButton、 QMUINavigationTitleView - QMUICMI.controlDisabledAlpha = 0.5f; // UIControlDisabledAlpha : UIControl 系列控件在 disabled 时的 alpha,默认用于 QMUIButton - -#pragma mark - UIButton - QMUICMI.buttonHighlightedAlpha = UIControlHighlightedAlpha; // ButtonHighlightedAlpha : QMUIButton 在 highlighted 时的 alpha,不影响系统的 UIButton - QMUICMI.buttonDisabledAlpha = UIControlDisabledAlpha; // ButtonDisabledAlpha : QMUIButton 在 disabled 时的 alpha,不影响系统的 UIButton - QMUICMI.buttonTintColor = self.themeTintColor; // ButtonTintColor : QMUIButton 默认的 tintColor,不影响系统的 UIButton - - QMUICMI.ghostButtonColorBlue = UIColorBlue; // GhostButtonColorBlue : QMUIGhostButtonColorBlue 的颜色 - QMUICMI.ghostButtonColorRed = UIColorRed; // GhostButtonColorRed : QMUIGhostButtonColorRed 的颜色 - QMUICMI.ghostButtonColorGreen = UIColorGreen; // GhostButtonColorGreen : QMUIGhostButtonColorGreen 的颜色 - QMUICMI.ghostButtonColorGray = UIColorGray; // GhostButtonColorGray : QMUIGhostButtonColorGray 的颜色 - QMUICMI.ghostButtonColorWhite = UIColorWhite; // GhostButtonColorWhite : QMUIGhostButtonColorWhite 的颜色 - - QMUICMI.fillButtonColorBlue = UIColorBlue; // FillButtonColorBlue : QMUIFillButtonColorBlue 的颜色 - QMUICMI.fillButtonColorRed = UIColorRed; // FillButtonColorRed : QMUIFillButtonColorRed 的颜色 - QMUICMI.fillButtonColorGreen = UIColorGreen; // FillButtonColorGreen : QMUIFillButtonColorGreen 的颜色 - QMUICMI.fillButtonColorGray = UIColorGray; // FillButtonColorGray : QMUIFillButtonColorGray 的颜色 - QMUICMI.fillButtonColorWhite = UIColorWhite; // FillButtonColorWhite : QMUIFillButtonColorWhite 的颜色 - - -#pragma mark - TextField & TextView - QMUICMI.textFieldTintColor = self.themeTintColor; // TextFieldTintColor : QMUITextField、QMUITextView 的 tintColor,不影响 UIKit 的输入框 - QMUICMI.textFieldTextInsets = UIEdgeInsetsMake(0, 7, 0, 7); // TextFieldTextInsets : QMUITextField 的内边距,不影响 UITextField - -#pragma mark - NavigationBar - - QMUICMI.navBarHighlightedAlpha = 0.2f; // NavBarHighlightedAlpha : QMUINavigationButton 在 highlighted 时的 alpha - QMUICMI.navBarDisabledAlpha = 0.2f; // NavBarDisabledAlpha : QMUINavigationButton 在 disabled 时的 alpha - QMUICMI.navBarButtonFont = UIFontMake(17); // NavBarButtonFont : QMUINavigationButtonTypeNormal 的字体(由于系统存在一些 bug,这个属性默认不对 UIBarButtonItem 生效) - QMUICMI.navBarButtonFontBold = UIFontBoldMake(17); // NavBarButtonFontBold : QMUINavigationButtonTypeBold 的字体 - QMUICMI.navBarBackgroundImage = [QDUIHelper navigationBarBackgroundImageWithThemeColor:self.themeTintColor]; // // NavBarBackgroundImage : UINavigationBar 的背景图,注意 navigationBar 的高度会受多个因素(是否全面屏、是否使用了 navigationItem.prompt、是否将 UISearchBar 作为 titleView)的影响,要检查各种情况是否都显示正常。 - QMUICMI.navBarShadowImage = [UIImage qmui_imageWithColor:UIColorClear size:CGSizeMake(4, PixelOne) cornerRadius:0]; // NavBarShadowImage : UINavigationBar.shadowImage,也即导航栏底部那条分隔线 - QMUICMI.navBarBarTintColor = nil; // NavBarBarTintColor : UINavigationBar.barTintColor,也即背景色 - QMUICMI.navBarTintColor = UIColorWhite; // NavBarTintColor : UINavigationBar 的 tintColor,也即导航栏上面的按钮颜色 - QMUICMI.navBarTitleColor = NavBarTintColor; // NavBarTitleColor : UINavigationBar 的标题颜色,以及 QMUINavigationTitleView 的默认文字颜色 - QMUICMI.navBarTitleFont = UIFontBoldMake(17); // NavBarTitleFont : UINavigationBar 的标题字体,以及 QMUINavigationTitleView 的默认字体 - QMUICMI.navBarLargeTitleColor = nil; // NavBarLargeTitleColor : UINavigationBar 在大标题模式下的标题颜色,仅在 iOS 11 之后才有效 - QMUICMI.navBarLargeTitleFont = nil; // NavBarLargeTitleFont : UINavigationBar 在大标题模式下的标题字体,仅在 iOS 11 之后才有效 - QMUICMI.navBarBackButtonTitlePositionAdjustment = UIOffsetZero; // NavBarBarBackButtonTitlePositionAdjustment : 导航栏返回按钮的文字偏移 - QMUICMI.sizeNavBarBackIndicatorImageAutomatically = YES; // SizeNavBarBackIndicatorImageAutomatically : 是否要自动调整 NavBarBackIndicatorImage 的 size 为 (13, 21) - QMUICMI.navBarBackIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavBack size:CGSizeMake(12, 20) tintColor:NavBarTintColor]; // NavBarBackIndicatorImage : 导航栏的返回按钮的图片,图片尺寸建议为(13, 21),否则最终的图片位置无法与系统原生的位置保持一致 - QMUICMI.navBarCloseButtonImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavClose size:CGSizeMake(16, 16) tintColor:NavBarTintColor]; // NavBarCloseButtonImage : QMUINavigationButton 用到的 × 的按钮图片 - - QMUICMI.navBarLoadingMarginRight = 3; // NavBarLoadingMarginRight : QMUINavigationTitleView 里左边 loading 的右边距 - QMUICMI.navBarAccessoryViewMarginLeft = 5; // NavBarAccessoryViewMarginLeft : QMUINavigationTitleView 里右边 accessoryView 的左边距 - QMUICMI.navBarActivityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;// NavBarActivityIndicatorViewStyle : QMUINavigationTitleView 里左边 loading 的主题 - QMUICMI.navBarAccessoryViewTypeDisclosureIndicatorImage = [[UIImage qmui_imageWithShape:QMUIImageShapeTriangle size:CGSizeMake(8, 5) tintColor:nil] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; // NavBarAccessoryViewTypeDisclosureIndicatorImage : QMUINavigationTitleView 右边箭头的图片 - -#pragma mark - TabBar - - QMUICMI.tabBarBackgroundImage = [[UIImage qmui_imageWithColor:UIColorMake(249, 249, 249)] resizableImageWithCapInsets:UIEdgeInsetsMake(1, 1, 1, 1)]; // TabBarBackgroundImage : UITabBar 的背景图,建议使用 resizableImage,否则在 UITabBar (NavigationController) 的 setBackgroundImage: 里会每次都视为 image 发生了变化(isEqual: 为 NO) - QMUICMI.tabBarBarTintColor = nil; // TabBarBarTintColor : UITabBar 的 barTintColor - QMUICMI.tabBarShadowImageColor = UIColorSeparator; // TabBarShadowImageColor : UITabBar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片 - QMUICMI.tabBarTintColor = self.themeTintColor; // TabBarTintColor : UITabBar 的 tintColor - QMUICMI.tabBarItemTitleColor = UIColorGray6; // TabBarItemTitleColor : 未选中的 UITabBarItem 的标题颜色 - QMUICMI.tabBarItemTitleColorSelected = TabBarTintColor; // TabBarItemTitleColorSelected : 选中的 UITabBarItem 的标题颜色 - QMUICMI.tabBarItemTitleFont = nil; // TabBarItemTitleFont : UITabBarItem 的标题字体 - -#pragma mark - Toolbar - - QMUICMI.toolBarHighlightedAlpha = 0.4f; // ToolBarHighlightedAlpha : QMUIToolbarButton 在 highlighted 状态下的 alpha - QMUICMI.toolBarDisabledAlpha = 0.4f; // ToolBarDisabledAlpha : QMUIToolbarButton 在 disabled 状态下的 alpha - QMUICMI.toolBarTintColor = UIColorBlue; // ToolBarTintColor : UIToolbar 的 tintColor,以及 QMUIToolbarButton normal 状态下的文字颜色 - QMUICMI.toolBarTintColorHighlighted = [ToolBarTintColor colorWithAlphaComponent:ToolBarHighlightedAlpha]; // ToolBarTintColorHighlighted : QMUIToolbarButton 在 highlighted 状态下的文字颜色 - QMUICMI.toolBarTintColorDisabled = [ToolBarTintColor colorWithAlphaComponent:ToolBarDisabledAlpha]; // ToolBarTintColorDisabled : QMUIToolbarButton 在 disabled 状态下的文字颜色 - QMUICMI.toolBarBackgroundImage = nil; // ToolBarBackgroundImage : UIToolbar 的背景图 - QMUICMI.toolBarBarTintColor = nil; // ToolBarBarTintColor : UIToolbar 的 tintColor - QMUICMI.toolBarShadowImageColor = UIColorSeparator; // ToolBarShadowImageColor : UIToolbar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片 - QMUICMI.toolBarButtonFont = UIFontMake(17); // ToolBarButtonFont : QMUIToolbarButton 的字体 - -#pragma mark - SearchBar - - QMUICMI.searchBarTextFieldBackground = UIColorMake(237, 238, 240); // SearchBarTextFieldBackground : QMUISearchBar 里的文本框的背景颜色 - QMUICMI.searchBarTextFieldBorderColor = nil; // SearchBarTextFieldBorderColor : QMUISearchBar 里的文本框的边框颜色 - QMUICMI.searchBarBottomBorderColor = nil; // SearchBarBottomBorderColor : QMUISearchBar 底部分隔线颜色 - QMUICMI.searchBarBarTintColor = UIColorWhite; // SearchBarBarTintColor : QMUISearchBar 的 barTintColor,也即背景色 - QMUICMI.searchBarTintColor = self.themeTintColor; // SearchBarTintColor : QMUISearchBar 的 tintColor,也即上面的操作控件的主题色 - QMUICMI.searchBarTextColor = UIColorBlack; // SearchBarTextColor : QMUISearchBar 里的文本框的文字颜色 - QMUICMI.searchBarPlaceholderColor = UIColorMake(136, 136, 143); // SearchBarPlaceholderColor : QMUISearchBar 里的文本框的 placeholder 颜色 - QMUICMI.searchBarFont = nil; // SearchBarFont : QMUISearchBar 里的文本框的文字字体及 placeholder 的字体 - QMUICMI.searchBarSearchIconImage = nil; // SearchBarSearchIconImage : QMUISearchBar 里的放大镜 icon - QMUICMI.searchBarClearIconImage = nil; // SearchBarClearIconImage : QMUISearchBar 里的文本框输入文字时右边的清空按钮的图片 - QMUICMI.searchBarTextFieldCornerRadius = 4.0; // SearchBarTextFieldCornerRadius : QMUISearchBar 里的文本框的圆角大小,-1 表示圆角大小为输入框高度的一半 - -#pragma mark - TableView / TableViewCell - - QMUICMI.tableViewEstimatedHeightEnabled = YES; // TableViewEstimatedHeightEnabled : 是否要开启全局 UITableView 的 estimatedRow(Section/Footer)Height - - QMUICMI.tableViewBackgroundColor = nil; // TableViewBackgroundColor : Plain 类型的 QMUITableView 的背景色颜色 - QMUICMI.tableViewGroupedBackgroundColor = UIColorMake(246, 246, 246); // TableViewGroupedBackgroundColor : Grouped 类型的 QMUITableView 的背景色 - QMUICMI.tableSectionIndexColor = UIColorGrayDarken; // TableSectionIndexColor : 列表右边的字母索引条的文字颜色 - QMUICMI.tableSectionIndexBackgroundColor = UIColorClear; // TableSectionIndexBackgroundColor : 列表右边的字母索引条的背景色 - QMUICMI.tableSectionIndexTrackingBackgroundColor = UIColorClear; // TableSectionIndexTrackingBackgroundColor : 列表右边的字母索引条在选中时的背景色 - QMUICMI.tableViewSeparatorColor = UIColorSeparator; // TableViewSeparatorColor : 列表的分隔线颜色 - - QMUICMI.tableViewCellNormalHeight = 56; // TableViewCellNormalHeight : QMUITableView 的默认 cell 高度 - QMUICMI.tableViewCellTitleLabelColor = UIColorGray3; // TableViewCellTitleLabelColor : QMUITableViewCell 的 textLabel 的文字颜色 - QMUICMI.tableViewCellDetailLabelColor = UIColorGray5; // TableViewCellDetailLabelColor : QMUITableViewCell 的 detailTextLabel 的文字颜色 - QMUICMI.tableViewCellBackgroundColor = UIColorWhite; // TableViewCellBackgroundColor : QMUITableViewCell 的背景色 - QMUICMI.tableViewCellSelectedBackgroundColor = UIColorMake(238, 239, 241); // TableViewCellSelectedBackgroundColor : QMUITableViewCell 点击时的背景色 - QMUICMI.tableViewCellWarningBackgroundColor = UIColorYellow; // TableViewCellWarningBackgroundColor : QMUITableViewCell 用于表示警告时的背景色,备用 - QMUICMI.tableViewCellDisclosureIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeDisclosureIndicator size:CGSizeMake(6, 10) lineWidth:1 tintColor:UIColorMake(173, 180, 190)]; // TableViewCellDisclosureIndicatorImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryDisclosureIndicator 时的箭头的图片 - QMUICMI.tableViewCellCheckmarkImage = [UIImage qmui_imageWithShape:QMUIImageShapeCheckmark size:CGSizeMake(15, 12) tintColor:self.themeTintColor]; // TableViewCellCheckmarkImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryCheckmark 时的打钩的图片 - QMUICMI.tableViewCellDetailButtonImage = [UIImage qmui_imageWithShape:QMUIImageShapeDetailButtonImage size:CGSizeMake(20, 20) tintColor:self.themeTintColor]; // TableViewCellDetailButtonImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryDetailButton 或 UITableViewCellAccessoryDetailDisclosureButton 时右边的 i 按钮图片 - QMUICMI.tableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator = 12; // TableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator : 列表 cell 右边的 i 按钮和向右箭头之间的间距(仅当两者都使用了自定义图片并且同时显示时才生效) - - QMUICMI.tableViewSectionHeaderBackgroundColor = UIColorMake(244, 244, 244); // TableViewSectionHeaderBackgroundColor : Plain 类型的 QMUITableView sectionHeader 的背景色 - QMUICMI.tableViewSectionFooterBackgroundColor = UIColorMake(244, 244, 244); // TableViewSectionFooterBackgroundColor : Plain 类型的 QMUITableView sectionFooter 的背景色 - QMUICMI.tableViewSectionHeaderFont = UIFontBoldMake(12); // TableViewSectionHeaderFont : Plain 类型的 QMUITableView sectionHeader 里的文字字体 - QMUICMI.tableViewSectionFooterFont = UIFontBoldMake(12); // TableViewSectionFooterFont : Plain 类型的 QMUITableView sectionFooter 里的文字字体 - QMUICMI.tableViewSectionHeaderTextColor = UIColorGray5; // TableViewSectionHeaderTextColor : Plain 类型的 QMUITableView sectionHeader 里的文字颜色 - QMUICMI.tableViewSectionFooterTextColor = UIColorGray; // TableViewSectionFooterTextColor : Plain 类型的 QMUITableView sectionFooter 里的文字颜色 - QMUICMI.tableViewSectionHeaderAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewSectionHeaderAccessoryMargins : Plain 类型的 QMUITableView sectionHeader accessoryView 的间距 - QMUICMI.tableViewSectionFooterAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewSectionFooterAccessoryMargins : Plain 类型的 QMUITableView sectionFooter accessoryView 的间距 - QMUICMI.tableViewSectionHeaderContentInset = UIEdgeInsetsMake(4, 15, 4, 15); // TableViewSectionHeaderContentInset : Plain 类型的 QMUITableView sectionHeader 里的内容的 padding - QMUICMI.tableViewSectionFooterContentInset = UIEdgeInsetsMake(4, 15, 4, 15); // TableViewSectionFooterContentInset : Plain 类型的 QMUITableView sectionFooter 里的内容的 padding - - QMUICMI.tableViewGroupedSectionHeaderFont = UIFontMake(12); // TableViewGroupedSectionHeaderFont : Grouped 类型的 QMUITableView sectionHeader 里的文字字体 - QMUICMI.tableViewGroupedSectionFooterFont = UIFontMake(12); // TableViewGroupedSectionFooterFont : Grouped 类型的 QMUITableView sectionFooter 里的文字字体 - QMUICMI.tableViewGroupedSectionHeaderTextColor = UIColorGrayDarken; // TableViewGroupedSectionHeaderTextColor : Grouped 类型的 QMUITableView sectionHeader 里的文字颜色 - QMUICMI.tableViewGroupedSectionFooterTextColor = UIColorGray; // TableViewGroupedSectionFooterTextColor : Grouped 类型的 QMUITableView sectionFooter 里的文字颜色 - QMUICMI.tableViewGroupedSectionHeaderAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewGroupedSectionHeaderAccessoryMargins : Grouped 类型的 QMUITableView sectionHeader accessoryView 的间距 - QMUICMI.tableViewGroupedSectionFooterAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewGroupedSectionFooterAccessoryMargins : Grouped 类型的 QMUITableView sectionFooter accessoryView 的间距 - QMUICMI.tableViewGroupedSectionHeaderDefaultHeight = UITableViewAutomaticDimension; // TableViewGroupedSectionHeaderDefaultHeight : Grouped 类型的 QMUITableView sectionHeader 的默认高度(也即没使用自定义的 sectionHeaderView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN - QMUICMI.tableViewGroupedSectionFooterDefaultHeight = UITableViewAutomaticDimension; // TableViewGroupedSectionFooterDefaultHeight : Grouped 类型的 QMUITableView sectionFooter 的默认高度(也即没使用自定义的 sectionFooterView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN - QMUICMI.tableViewGroupedSectionHeaderContentInset = UIEdgeInsetsMake(16, PreferredValueForiPhone(20, 15, 15, 15), 8, PreferredValueForiPhone(20, 15, 15, 15)); // TableViewGroupedSectionHeaderContentInset : Grouped 类型的 QMUITableView sectionHeader 里的内容的 padding - QMUICMI.tableViewGroupedSectionFooterContentInset = UIEdgeInsetsMake(8, 15, 2, 15); // TableViewGroupedSectionFooterContentInset : Grouped 类型的 QMUITableView sectionFooter 里的内容的 padding - -#pragma mark - UIWindowLevel - QMUICMI.windowLevelQMUIAlertView = UIWindowLevelAlert - 4.0; // UIWindowLevelQMUIAlertView : QMUIModalPresentationViewController、QMUIPopupContainerView 里使用的 UIWindow 的 windowLevel - -#pragma mark - QMUILog - QMUICMI.shouldPrintDefaultLog = YES; // ShouldPrintDefaultLog : 是否允许输出 QMUILogLevelDefault 级别的 log - QMUICMI.shouldPrintInfoLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelInfo 级别的 log - QMUICMI.shouldPrintWarnLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelWarn 级别的 log - -#pragma mark - QMUIBadge - - QMUICMI.badgeBackgroundColor = UIColorRed; // BadgeBackgroundColor : UIBarButtoItem、UITabBarItem 上的未读数的背景色 - QMUICMI.badgeTextColor = UIColorWhite; // BadgeTextColor : UIBarButtoItem、UITabBarItem 上的未读数的文字颜色 - QMUICMI.badgeFont = UIFontBoldMake(11); // BadgeFont : UIBarButtoItem、UITabBarItem 上的未读数的字体 - QMUICMI.badgeContentEdgeInsets = UIEdgeInsetsMake(2, 4, 2, 4); // BadgeContentEdgeInsets : UIBarButtoItem、UITabBarItem 上的未读数与圆圈之间的 padding - QMUICMI.badgeCenterOffset = CGPointMake(14, -10); // BadgeCenterOffset : UIBarButtoItem、UITabBarItem 上的未读数相对于 item 中心的偏移 - QMUICMI.badgeCenterOffsetLandscape = CGPointMake(16, -7); // BadgeCenterOffsetLandscape : UIBarButtoItem、UITabBarItem 上的未读数在横屏下相对于 item 中心的偏移 - - QMUICMI.updatesIndicatorColor = UIColorRed; // UpdatesIndicatorColor : UIBarButtoItem、UITabBarItem 上的未读红点的颜色 - QMUICMI.updatesIndicatorSize = CGSizeMake(7, 7); // UpdatesIndicatorSize : UIBarButtoItem、UITabBarItem 上的未读红点的大小 - QMUICMI.updatesIndicatorCenterOffset = CGPointMake(14, -10); // UpdatesIndicatorCenterOffset : UIBarButtoItem、UITabBarItem 上的未读红点相对于 item 中心的偏移 - QMUICMI.updatesIndicatorCenterOffsetLandscape = CGPointMake(14, -10); // UpdatesIndicatorCenterOffsetLandscape : UIBarButtoItem、UITabBarItem 上的未读红点在横屏下相对于 item 中心的偏移 - -#pragma mark - Others - - QMUICMI.automaticCustomNavigationBarTransitionStyle = NO; // AutomaticCustomNavigationBarTransitionStyle : 界面 push/pop 时是否要自动根据两个界面的 barTintColor/backgroundImage/shadowImage 的样式差异来决定是否使用自定义的导航栏效果 - QMUICMI.supportedOrientationMask = UIInterfaceOrientationMaskAll; // SupportedOrientationMask : 默认支持的横竖屏方向 - QMUICMI.automaticallyRotateDeviceOrientation = YES; // AutomaticallyRotateDeviceOrientation : 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕 - QMUICMI.statusbarStyleLightInitially = YES; // StatusbarStyleLightInitially : 默认的状态栏内容是否使用白色,默认为 NO,也即黑色 - QMUICMI.needsBackBarButtonItemTitle = NO; // NeedsBackBarButtonItemTitle : 全局是否需要返回按钮的 title,不需要则只显示一个返回image - QMUICMI.hidesBottomBarWhenPushedInitially = YES; // HidesBottomBarWhenPushedInitially : QMUICommonViewController.hidesBottomBarWhenPushed 的初始值,默认为 NO,以保持与系统默认值一致,但通常建议改为 YES,因为一般只有 tabBar 首页那几个界面要求为 NO - QMUICMI.preventConcurrentNavigationControllerTransitions = YES; // PreventConcurrentNavigationControllerTransitions : 自动保护 QMUINavigationController 在上一次 push/pop 尚未结束的时候就进行下一次 push/pop 的行为,避免产生 crash - QMUICMI.navigationBarHiddenInitially = NO; // NavigationBarHiddenInitially : QMUINavigationControllerDelegate preferredNavigationBarHidden 的初始值,默认为NO - QMUICMI.shouldFixTabBarTransitionBugInIPhoneX = YES; // ShouldFixTabBarTransitionBugInIPhoneX : 是否需要自动修复 iOS 11 下,iPhone X 的设备在 push 界面时,tabBar 会瞬间往上跳的 bug - QMUICMI.shouldFixTabBarButtonBugForAll = NO; // ShouldFixTabBarButtonBugForAll : 是否要对 iOS 12.1.1 及以后的版本也修复手势返回时 tabBarButton 布局错误的 bug(issue #410),默认为 NO - QMUICMI.shouldPrintQMUIWarnLogToConsole = YES; // ShouldPrintQMUIWarnLogToConsole : 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上 - QMUICMI.sendAnalyticsToQMUITeam = YES; // SendAnalyticsToQMUITeam : 是否允许在 DEBUG 模式下上报 Bundle Identifier 和 Display Name 给 QMUI 统计用 -} - // QMUI 2.3.0 版本里,配置表新增这个方法,返回 YES 表示在 App 启动时要自动应用这份配置表。仅当你的 App 里存在多份配置表时,才需要把除默认配置表之外的其他配置表的返回值改为 NO。 - (BOOL)shouldApplyTemplateAutomatically { - BOOL result = [[[NSUserDefaults standardUserDefaults] stringForKey:QDSelectedThemeClassName] isEqualToString:NSStringFromClass(self.class)]; + [QMUIThemeManagerCenter.defaultThemeManager addThemeIdentifier:self.themeName theme:self]; + + NSString *selectedThemeIdentifier = [[NSUserDefaults standardUserDefaults] stringForKey:QDSelectedThemeIdentifier]; + BOOL result = [selectedThemeIdentifier isEqualToString:self.themeName]; if (result) { - [QDThemeManager sharedInstance].currentTheme = self; + QMUIThemeManagerCenter.defaultThemeManager.currentTheme = self; } return result; } @@ -229,20 +30,8 @@ - (UIColor *)themeTintColor { return UIColorTheme1; } -- (UIColor *)themeListTextColor { - return self.themeTintColor; -} - -- (UIColor *)themeCodeColor { - return self.themeTintColor; -} - -- (UIColor *)themeGridItemTintColor { - return self.themeTintColor; -} - - (NSString *)themeName { - return @"Grapefruit"; + return QDThemeIdentifierGrapefruit; } @end diff --git a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrass.h b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrass.h index fa47a886..e3ff403c 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrass.h +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrass.h @@ -5,17 +5,8 @@ // Copyright (c) 2015年 QMUI Team. All rights reserved. // -#import -#import "QDThemeProtocol.h" +#import "QMUIConfigurationTemplate.h" -/** - * QMUIConfigurationTemplate 是一份配置表,用于配合 QMUIConfiguration 来管理整个 App 的全局样式,使用方式: - * 在 QMUI 项目代码的文件夹里找到 QMUIConfigurationTemplate 目录,把里面所有文件复制到自己项目里,保证能被编译到即可,不需要在某些地方 import,也不需要手动运行。 - * - * @warning 更新 QMUIKit 的版本时,请留意 Release Log 里是否有提醒更新配置表,请尽量保持自己项目里的配置表与 QMUIKit 里的配置表一致,避免遗漏新的属性。 - * @warning 配置表的 class 名必须以 QMUIConfigurationTemplate 开头,并且实现 ,因为这两者是 QMUI 识别该 NSObject 是否为一份配置表的条件。 - * @warning QMUI 2.3.0 之后,配置表改为自动运行,不需要再在某个地方手动运行了。 - */ -@interface QMUIConfigurationTemplateGrass : NSObject +@interface QMUIConfigurationTemplateGrass : QMUIConfigurationTemplate @end diff --git a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrass.m b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrass.m index 700b712e..24b02241 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrass.m +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrass.m @@ -7,218 +7,19 @@ // #import "QMUIConfigurationTemplateGrass.h" -#import @implementation QMUIConfigurationTemplateGrass #pragma mark - -- (void)applyConfigurationTemplate { - - // === 修改配置值 === // - -#pragma mark - Global Color - - QMUICMI.clearColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0]; // UIColorClear : 透明色 - QMUICMI.whiteColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:1]; // UIColorWhite : 白色(不用 [UIColor whiteColor] 是希望保持颜色空间为 RGB) - QMUICMI.blackColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:1]; // UIColorBlack : 黑色(不用 [UIColor blackColor] 是希望保持颜色空间为 RGB) - QMUICMI.grayColor = UIColorGray4; // UIColorGray : 最常用的灰色 - QMUICMI.grayDarkenColor = UIColorGray3; // UIColorGrayDarken : 深一点的灰色 - QMUICMI.grayLightenColor = UIColorGray7; // UIColorGrayLighten : 浅一点的灰色 - QMUICMI.redColor = UIColorMake(250, 58, 58); // UIColorRed : 红色 - QMUICMI.greenColor = UIColorTheme4; // UIColorGreen : 绿色 - QMUICMI.blueColor = UIColorMake(49, 189, 243); // UIColorBlue : 蓝色 - QMUICMI.yellowColor = UIColorTheme3; // UIColorYellow : 黄色 - - QMUICMI.linkColor = UIColorMake(56, 116, 171); // UIColorLink : 文字链接颜色 - QMUICMI.disabledColor = UIColorGray; // UIColorDisabled : 全局 disabled 的颜色,一般用于 UIControl 等控件 - QMUICMI.backgroundColor = UIColorWhite; // UIColorForBackground : 界面背景色,默认用于 QMUICommonViewController.view 的背景色 - QMUICMI.maskDarkColor = UIColorMakeWithRGBA(0, 0, 0, .35f); // UIColorMask : 深色的背景遮罩,默认用于 QMAlertController、QMUIDialogViewController 等弹出控件的遮罩 - QMUICMI.maskLightColor = UIColorMakeWithRGBA(255, 255, 255, .5f); // UIColorMaskWhite : 浅色的背景遮罩,QMUIKit 里默认没用到,只是占个位 - QMUICMI.separatorColor = UIColorMake(222, 224, 226); // UIColorSeparator : 全局默认的分割线颜色,默认用于列表分隔线颜色、UIView (QMUI_Border) 分隔线颜色 - QMUICMI.separatorDashedColor = UIColorMake(17, 17, 17); // UIColorSeparatorDashed : 全局默认的虚线分隔线的颜色,默认 QMUIKit 暂时没用到 - QMUICMI.placeholderColor = UIColorGray8; // UIColorPlaceholder,全局的输入框的 placeholder 颜色,默认用于 QMUITextField、QMUITextView,不影响系统 UIKit 的输入框 - - // 测试用的颜色 - QMUICMI.testColorRed = UIColorMakeWithRGBA(255, 0, 0, .3); - QMUICMI.testColorGreen = UIColorMakeWithRGBA(0, 255, 0, .3); - QMUICMI.testColorBlue = UIColorMakeWithRGBA(0, 0, 255, .3); - - -#pragma mark - UIControl - - QMUICMI.controlHighlightedAlpha = 0.5f; // UIControlHighlightedAlpha : UIControl 系列控件在 highlighted 时的 alpha,默认用于 QMUIButton、 QMUINavigationTitleView - QMUICMI.controlDisabledAlpha = 0.5f; // UIControlDisabledAlpha : UIControl 系列控件在 disabled 时的 alpha,默认用于 QMUIButton - -#pragma mark - UIButton - QMUICMI.buttonHighlightedAlpha = UIControlHighlightedAlpha; // ButtonHighlightedAlpha : QMUIButton 在 highlighted 时的 alpha,不影响系统的 UIButton - QMUICMI.buttonDisabledAlpha = UIControlDisabledAlpha; // ButtonDisabledAlpha : QMUIButton 在 disabled 时的 alpha,不影响系统的 UIButton - QMUICMI.buttonTintColor = self.themeTintColor; // ButtonTintColor : QMUIButton 默认的 tintColor,不影响系统的 UIButton - - QMUICMI.ghostButtonColorBlue = UIColorBlue; // GhostButtonColorBlue : QMUIGhostButtonColorBlue 的颜色 - QMUICMI.ghostButtonColorRed = UIColorRed; // GhostButtonColorRed : QMUIGhostButtonColorRed 的颜色 - QMUICMI.ghostButtonColorGreen = UIColorGreen; // GhostButtonColorGreen : QMUIGhostButtonColorGreen 的颜色 - QMUICMI.ghostButtonColorGray = UIColorGray; // GhostButtonColorGray : QMUIGhostButtonColorGray 的颜色 - QMUICMI.ghostButtonColorWhite = UIColorWhite; // GhostButtonColorWhite : QMUIGhostButtonColorWhite 的颜色 - - QMUICMI.fillButtonColorBlue = UIColorBlue; // FillButtonColorBlue : QMUIFillButtonColorBlue 的颜色 - QMUICMI.fillButtonColorRed = UIColorRed; // FillButtonColorRed : QMUIFillButtonColorRed 的颜色 - QMUICMI.fillButtonColorGreen = UIColorGreen; // FillButtonColorGreen : QMUIFillButtonColorGreen 的颜色 - QMUICMI.fillButtonColorGray = UIColorGray; // FillButtonColorGray : QMUIFillButtonColorGray 的颜色 - QMUICMI.fillButtonColorWhite = UIColorWhite; // FillButtonColorWhite : QMUIFillButtonColorWhite 的颜色 - - -#pragma mark - TextField & TextView - QMUICMI.textFieldTintColor = self.themeTintColor; // TextFieldTintColor : QMUITextField、QMUITextView 的 tintColor,不影响 UIKit 的输入框 - QMUICMI.textFieldTextInsets = UIEdgeInsetsMake(0, 7, 0, 7); // TextFieldTextInsets : QMUITextField 的内边距,不影响 UITextField - -#pragma mark - NavigationBar - - QMUICMI.navBarHighlightedAlpha = 0.2f; // NavBarHighlightedAlpha : QMUINavigationButton 在 highlighted 时的 alpha - QMUICMI.navBarDisabledAlpha = 0.2f; // NavBarDisabledAlpha : QMUINavigationButton 在 disabled 时的 alpha - QMUICMI.navBarButtonFont = UIFontMake(17); // NavBarButtonFont : QMUINavigationButtonTypeNormal 的字体(由于系统存在一些 bug,这个属性默认不对 UIBarButtonItem 生效) - QMUICMI.navBarButtonFontBold = UIFontBoldMake(17); // NavBarButtonFontBold : QMUINavigationButtonTypeBold 的字体 - QMUICMI.navBarBackgroundImage = [QDUIHelper navigationBarBackgroundImageWithThemeColor:self.themeTintColor]; // NavBarBackgroundImage : UINavigationBar 的背景图,注意 navigationBar 的高度会受多个因素(是否全面屏、是否使用了 navigationItem.prompt、是否将 UISearchBar 作为 titleView)的影响,要检查各种情况是否都显示正常。 - QMUICMI.navBarShadowImage = [UIImage qmui_imageWithColor:UIColorClear size:CGSizeMake(4, PixelOne) cornerRadius:0]; // NavBarShadowImage : UINavigationBar.shadowImage,也即导航栏底部那条分隔线 - QMUICMI.navBarBarTintColor = nil; // NavBarBarTintColor : UINavigationBar.barTintColor,也即背景色 - QMUICMI.navBarTintColor = UIColorWhite; // NavBarTintColor : UINavigationBar 的 tintColor,也即导航栏上面的按钮颜色 - QMUICMI.navBarTitleColor = NavBarTintColor; // NavBarTitleColor : UINavigationBar 的标题颜色,以及 QMUINavigationTitleView 的默认文字颜色 - QMUICMI.navBarTitleFont = UIFontBoldMake(17); // NavBarTitleFont : UINavigationBar 的标题字体,以及 QMUINavigationTitleView 的默认字体 - QMUICMI.navBarLargeTitleColor = nil; // NavBarLargeTitleColor : UINavigationBar 在大标题模式下的标题颜色,仅在 iOS 11 之后才有效 - QMUICMI.navBarLargeTitleFont = nil; // NavBarLargeTitleFont : UINavigationBar 在大标题模式下的标题字体,仅在 iOS 11 之后才有效 - QMUICMI.navBarBackButtonTitlePositionAdjustment = UIOffsetZero; // NavBarBarBackButtonTitlePositionAdjustment : 导航栏返回按钮的文字偏移 - QMUICMI.sizeNavBarBackIndicatorImageAutomatically = YES; // SizeNavBarBackIndicatorImageAutomatically : 是否要自动调整 NavBarBackIndicatorImage 的 size 为 (13, 21) - QMUICMI.navBarBackIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavBack size:CGSizeMake(12, 20) tintColor:NavBarTintColor]; // NavBarBackIndicatorImage : 导航栏的返回按钮的图片,图片尺寸建议为(13, 21),否则最终的图片位置无法与系统原生的位置保持一致 - QMUICMI.navBarCloseButtonImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavClose size:CGSizeMake(16, 16) tintColor:NavBarTintColor]; // NavBarCloseButtonImage : QMUINavigationButton 用到的 × 的按钮图片 - - QMUICMI.navBarLoadingMarginRight = 3; // NavBarLoadingMarginRight : QMUINavigationTitleView 里左边 loading 的右边距 - QMUICMI.navBarAccessoryViewMarginLeft = 5; // NavBarAccessoryViewMarginLeft : QMUINavigationTitleView 里右边 accessoryView 的左边距 - QMUICMI.navBarActivityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;// NavBarActivityIndicatorViewStyle : QMUINavigationTitleView 里左边 loading 的主题 - QMUICMI.navBarAccessoryViewTypeDisclosureIndicatorImage = [[UIImage qmui_imageWithShape:QMUIImageShapeTriangle size:CGSizeMake(8, 5) tintColor:nil] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; // NavBarAccessoryViewTypeDisclosureIndicatorImage : QMUINavigationTitleView 右边箭头的图片 - -#pragma mark - TabBar - - QMUICMI.tabBarBackgroundImage = [[UIImage qmui_imageWithColor:UIColorMake(249, 249, 249)] resizableImageWithCapInsets:UIEdgeInsetsMake(1, 1, 1, 1)]; // TabBarBackgroundImage : UITabBar 的背景图,建议使用 resizableImage,否则在 UITabBar (NavigationController) 的 setBackgroundImage: 里会每次都视为 image 发生了变化(isEqual: 为 NO) - QMUICMI.tabBarBarTintColor = nil; // TabBarBarTintColor : UITabBar 的 barTintColor - QMUICMI.tabBarShadowImageColor = UIColorSeparator; // TabBarShadowImageColor : UITabBar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片 - QMUICMI.tabBarTintColor = self.themeTintColor; // TabBarTintColor : UITabBar 的 tintColor - QMUICMI.tabBarItemTitleColor = UIColorGray6; // TabBarItemTitleColor : 未选中的 UITabBarItem 的标题颜色 - QMUICMI.tabBarItemTitleColorSelected = TabBarTintColor; // TabBarItemTitleColorSelected : 选中的 UITabBarItem 的标题颜色 - QMUICMI.tabBarItemTitleFont = nil; // TabBarItemTitleFont : UITabBarItem 的标题字体 - -#pragma mark - Toolbar - - QMUICMI.toolBarHighlightedAlpha = 0.4f; // ToolBarHighlightedAlpha : QMUIToolbarButton 在 highlighted 状态下的 alpha - QMUICMI.toolBarDisabledAlpha = 0.4f; // ToolBarDisabledAlpha : QMUIToolbarButton 在 disabled 状态下的 alpha - QMUICMI.toolBarTintColor = UIColorBlue; // ToolBarTintColor : UIToolbar 的 tintColor,以及 QMUIToolbarButton normal 状态下的文字颜色 - QMUICMI.toolBarTintColorHighlighted = [ToolBarTintColor colorWithAlphaComponent:ToolBarHighlightedAlpha]; // ToolBarTintColorHighlighted : QMUIToolbarButton 在 highlighted 状态下的文字颜色 - QMUICMI.toolBarTintColorDisabled = [ToolBarTintColor colorWithAlphaComponent:ToolBarDisabledAlpha]; // ToolBarTintColorDisabled : QMUIToolbarButton 在 disabled 状态下的文字颜色 - QMUICMI.toolBarBackgroundImage = nil; // ToolBarBackgroundImage : UIToolbar 的背景图 - QMUICMI.toolBarBarTintColor = nil; // ToolBarBarTintColor : UIToolbar 的 tintColor - QMUICMI.toolBarShadowImageColor = UIColorSeparator; // ToolBarShadowImageColor : UIToolbar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片 - QMUICMI.toolBarButtonFont = UIFontMake(17); // ToolBarButtonFont : QMUIToolbarButton 的字体 - -#pragma mark - SearchBar - - QMUICMI.searchBarTextFieldBackground = UIColorMake(237, 238, 240); // SearchBarTextFieldBackground : QMUISearchBar 里的文本框的背景颜色 - QMUICMI.searchBarTextFieldBorderColor = nil; // SearchBarTextFieldBorderColor : QMUISearchBar 里的文本框的边框颜色 - QMUICMI.searchBarBottomBorderColor = nil; // SearchBarBottomBorderColor : QMUISearchBar 底部分隔线颜色 - QMUICMI.searchBarBarTintColor = UIColorWhite; // SearchBarBarTintColor : QMUISearchBar 的 barTintColor,也即背景色 - QMUICMI.searchBarTintColor = self.themeTintColor; // SearchBarTintColor : QMUISearchBar 的 tintColor,也即上面的操作控件的主题色 - QMUICMI.searchBarTextColor = UIColorBlack; // SearchBarTextColor : QMUISearchBar 里的文本框的文字颜色 - QMUICMI.searchBarPlaceholderColor = UIColorMake(136, 136, 143); // SearchBarPlaceholderColor : QMUISearchBar 里的文本框的 placeholder 颜色 - QMUICMI.searchBarFont = nil; // SearchBarFont : QMUISearchBar 里的文本框的文字字体及 placeholder 的字体 - QMUICMI.searchBarSearchIconImage = nil; // SearchBarSearchIconImage : QMUISearchBar 里的放大镜 icon - QMUICMI.searchBarClearIconImage = nil; // SearchBarClearIconImage : QMUISearchBar 里的文本框输入文字时右边的清空按钮的图片 - QMUICMI.searchBarTextFieldCornerRadius = 4.0; // SearchBarTextFieldCornerRadius : QMUISearchBar 里的文本框的圆角大小,-1 表示圆角大小为输入框高度的一半 - -#pragma mark - TableView / TableViewCell - - QMUICMI.tableViewEstimatedHeightEnabled = YES; // TableViewEstimatedHeightEnabled : 是否要开启全局 UITableView 的 estimatedRow(Section/Footer)Height - - QMUICMI.tableViewBackgroundColor = nil; // TableViewBackgroundColor : Plain 类型的 QMUITableView 的背景色颜色 - QMUICMI.tableViewGroupedBackgroundColor = UIColorMake(246, 246, 246); // TableViewGroupedBackgroundColor : Grouped 类型的 QMUITableView 的背景色 - QMUICMI.tableSectionIndexColor = UIColorGrayDarken; // TableSectionIndexColor : 列表右边的字母索引条的文字颜色 - QMUICMI.tableSectionIndexBackgroundColor = UIColorClear; // TableSectionIndexBackgroundColor : 列表右边的字母索引条的背景色 - QMUICMI.tableSectionIndexTrackingBackgroundColor = UIColorClear; // TableSectionIndexTrackingBackgroundColor : 列表右边的字母索引条在选中时的背景色 - QMUICMI.tableViewSeparatorColor = UIColorSeparator; // TableViewSeparatorColor : 列表的分隔线颜色 - - QMUICMI.tableViewCellNormalHeight = 56; // TableViewCellNormalHeight : QMUITableView 的默认 cell 高度 - QMUICMI.tableViewCellTitleLabelColor = UIColorGray3; // TableViewCellTitleLabelColor : QMUITableViewCell 的 textLabel 的文字颜色 - QMUICMI.tableViewCellDetailLabelColor = UIColorGray5; // TableViewCellDetailLabelColor : QMUITableViewCell 的 detailTextLabel 的文字颜色 - QMUICMI.tableViewCellBackgroundColor = UIColorWhite; // TableViewCellBackgroundColor : QMUITableViewCell 的背景色 - QMUICMI.tableViewCellSelectedBackgroundColor = UIColorMake(238, 239, 241); // TableViewCellSelectedBackgroundColor : QMUITableViewCell 点击时的背景色 - QMUICMI.tableViewCellWarningBackgroundColor = UIColorYellow; // TableViewCellWarningBackgroundColor : QMUITableViewCell 用于表示警告时的背景色,备用 - QMUICMI.tableViewCellDisclosureIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeDisclosureIndicator size:CGSizeMake(6, 10) lineWidth:1 tintColor:UIColorMake(173, 180, 190)]; // TableViewCellDisclosureIndicatorImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryDisclosureIndicator 时的箭头的图片 - QMUICMI.tableViewCellCheckmarkImage = [UIImage qmui_imageWithShape:QMUIImageShapeCheckmark size:CGSizeMake(15, 12) tintColor:self.themeTintColor]; // TableViewCellCheckmarkImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryCheckmark 时的打钩的图片 - QMUICMI.tableViewCellDetailButtonImage = [UIImage qmui_imageWithShape:QMUIImageShapeDetailButtonImage size:CGSizeMake(20, 20) tintColor:self.themeTintColor]; // TableViewCellDetailButtonImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryDetailButton 或 UITableViewCellAccessoryDetailDisclosureButton 时右边的 i 按钮图片 - QMUICMI.tableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator = 12; // TableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator : 列表 cell 右边的 i 按钮和向右箭头之间的间距(仅当两者都使用了自定义图片并且同时显示时才生效) - - QMUICMI.tableViewSectionHeaderBackgroundColor = UIColorMake(244, 244, 244); // TableViewSectionHeaderBackgroundColor : Plain 类型的 QMUITableView sectionHeader 的背景色 - QMUICMI.tableViewSectionFooterBackgroundColor = UIColorMake(244, 244, 244); // TableViewSectionFooterBackgroundColor : Plain 类型的 QMUITableView sectionFooter 的背景色 - QMUICMI.tableViewSectionHeaderFont = UIFontBoldMake(12); // TableViewSectionHeaderFont : Plain 类型的 QMUITableView sectionHeader 里的文字字体 - QMUICMI.tableViewSectionFooterFont = UIFontBoldMake(12); // TableViewSectionFooterFont : Plain 类型的 QMUITableView sectionFooter 里的文字字体 - QMUICMI.tableViewSectionHeaderTextColor = UIColorGray5; // TableViewSectionHeaderTextColor : Plain 类型的 QMUITableView sectionHeader 里的文字颜色 - QMUICMI.tableViewSectionFooterTextColor = UIColorGray; // TableViewSectionFooterTextColor : Plain 类型的 QMUITableView sectionFooter 里的文字颜色 - QMUICMI.tableViewSectionHeaderAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewSectionHeaderAccessoryMargins : Plain 类型的 QMUITableView sectionHeader accessoryView 的间距 - QMUICMI.tableViewSectionFooterAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewSectionFooterAccessoryMargins : Plain 类型的 QMUITableView sectionFooter accessoryView 的间距 - QMUICMI.tableViewSectionHeaderContentInset = UIEdgeInsetsMake(4, 15, 4, 15); // TableViewSectionHeaderContentInset : Plain 类型的 QMUITableView sectionHeader 里的内容的 padding - QMUICMI.tableViewSectionFooterContentInset = UIEdgeInsetsMake(4, 15, 4, 15); // TableViewSectionFooterContentInset : Plain 类型的 QMUITableView sectionFooter 里的内容的 padding - - QMUICMI.tableViewGroupedSectionHeaderFont = UIFontMake(12); // TableViewGroupedSectionHeaderFont : Grouped 类型的 QMUITableView sectionHeader 里的文字字体 - QMUICMI.tableViewGroupedSectionFooterFont = UIFontMake(12); // TableViewGroupedSectionFooterFont : Grouped 类型的 QMUITableView sectionFooter 里的文字字体 - QMUICMI.tableViewGroupedSectionHeaderTextColor = UIColorGrayDarken; // TableViewGroupedSectionHeaderTextColor : Grouped 类型的 QMUITableView sectionHeader 里的文字颜色 - QMUICMI.tableViewGroupedSectionFooterTextColor = UIColorGray; // TableViewGroupedSectionFooterTextColor : Grouped 类型的 QMUITableView sectionFooter 里的文字颜色 - QMUICMI.tableViewGroupedSectionHeaderAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewGroupedSectionHeaderAccessoryMargins : Grouped 类型的 QMUITableView sectionHeader accessoryView 的间距 - QMUICMI.tableViewGroupedSectionFooterAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewGroupedSectionFooterAccessoryMargins : Grouped 类型的 QMUITableView sectionFooter accessoryView 的间距 - QMUICMI.tableViewGroupedSectionHeaderDefaultHeight = UITableViewAutomaticDimension; // TableViewGroupedSectionHeaderDefaultHeight : Grouped 类型的 QMUITableView sectionHeader 的默认高度(也即没使用自定义的 sectionHeaderView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN - QMUICMI.tableViewGroupedSectionFooterDefaultHeight = UITableViewAutomaticDimension; // TableViewGroupedSectionFooterDefaultHeight : Grouped 类型的 QMUITableView sectionFooter 的默认高度(也即没使用自定义的 sectionFooterView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN - QMUICMI.tableViewGroupedSectionHeaderContentInset = UIEdgeInsetsMake(16, PreferredValueForiPhone(20, 15, 15, 15), 8, PreferredValueForiPhone(20, 15, 15, 15)); // TableViewGroupedSectionHeaderContentInset : Grouped 类型的 QMUITableView sectionHeader 里的内容的 padding - QMUICMI.tableViewGroupedSectionFooterContentInset = UIEdgeInsetsMake(8, 15, 2, 15); // TableViewGroupedSectionFooterContentInset : Grouped 类型的 QMUITableView sectionFooter 里的内容的 padding - -#pragma mark - UIWindowLevel - QMUICMI.windowLevelQMUIAlertView = UIWindowLevelAlert - 4.0; // UIWindowLevelQMUIAlertView : QMUIModalPresentationViewController、QMUIPopupContainerView 里使用的 UIWindow 的 windowLevel - -#pragma mark - QMUILog - QMUICMI.shouldPrintDefaultLog = YES; // ShouldPrintDefaultLog : 是否允许输出 QMUILogLevelDefault 级别的 log - QMUICMI.shouldPrintInfoLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelInfo 级别的 log - QMUICMI.shouldPrintWarnLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelWarn 级别的 log - -#pragma mark - QMUIBadge - - QMUICMI.badgeBackgroundColor = UIColorRed; // BadgeBackgroundColor : UIBarButtoItem、UITabBarItem 上的未读数的背景色 - QMUICMI.badgeTextColor = UIColorWhite; // BadgeTextColor : UIBarButtoItem、UITabBarItem 上的未读数的文字颜色 - QMUICMI.badgeFont = UIFontBoldMake(11); // BadgeFont : UIBarButtoItem、UITabBarItem 上的未读数的字体 - QMUICMI.badgeContentEdgeInsets = UIEdgeInsetsMake(2, 4, 2, 4); // BadgeContentEdgeInsets : UIBarButtoItem、UITabBarItem 上的未读数与圆圈之间的 padding - QMUICMI.badgeCenterOffset = CGPointMake(14, -10); // BadgeCenterOffset : UIBarButtoItem、UITabBarItem 上的未读数相对于 item 中心的偏移 - QMUICMI.badgeCenterOffsetLandscape = CGPointMake(16, -7); // BadgeCenterOffsetLandscape : UIBarButtoItem、UITabBarItem 上的未读数在横屏下相对于 item 中心的偏移 - - QMUICMI.updatesIndicatorColor = UIColorRed; // UpdatesIndicatorColor : UIBarButtoItem、UITabBarItem 上的未读红点的颜色 - QMUICMI.updatesIndicatorSize = CGSizeMake(7, 7); // UpdatesIndicatorSize : UIBarButtoItem、UITabBarItem 上的未读红点的大小 - QMUICMI.updatesIndicatorCenterOffset = CGPointMake(14, -10); // UpdatesIndicatorCenterOffset : UIBarButtoItem、UITabBarItem 上的未读红点相对于 item 中心的偏移 - QMUICMI.updatesIndicatorCenterOffsetLandscape = CGPointMake(14, -10); // UpdatesIndicatorCenterOffsetLandscape : UIBarButtoItem、UITabBarItem 上的未读红点在横屏下相对于 item 中心的偏移 - -#pragma mark - Others - - QMUICMI.automaticCustomNavigationBarTransitionStyle = NO; // AutomaticCustomNavigationBarTransitionStyle : 界面 push/pop 时是否要自动根据两个界面的 barTintColor/backgroundImage/shadowImage 的样式差异来决定是否使用自定义的导航栏效果 - QMUICMI.supportedOrientationMask = UIInterfaceOrientationMaskAll; // SupportedOrientationMask : 默认支持的横竖屏方向 - QMUICMI.automaticallyRotateDeviceOrientation = YES; // AutomaticallyRotateDeviceOrientation : 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕 - QMUICMI.statusbarStyleLightInitially = YES; // StatusbarStyleLightInitially : 默认的状态栏内容是否使用白色,默认为 NO,也即黑色 - QMUICMI.needsBackBarButtonItemTitle = NO; // NeedsBackBarButtonItemTitle : 全局是否需要返回按钮的 title,不需要则只显示一个返回image - QMUICMI.hidesBottomBarWhenPushedInitially = YES; // HidesBottomBarWhenPushedInitially : QMUICommonViewController.hidesBottomBarWhenPushed 的初始值,默认为 NO,以保持与系统默认值一致,但通常建议改为 YES,因为一般只有 tabBar 首页那几个界面要求为 NO - QMUICMI.preventConcurrentNavigationControllerTransitions = YES; // PreventConcurrentNavigationControllerTransitions : 自动保护 QMUINavigationController 在上一次 push/pop 尚未结束的时候就进行下一次 push/pop 的行为,避免产生 crash - QMUICMI.navigationBarHiddenInitially = NO; // NavigationBarHiddenInitially : QMUINavigationControllerDelegate preferredNavigationBarHidden 的初始值,默认为NO - QMUICMI.shouldFixTabBarTransitionBugInIPhoneX = YES; // ShouldFixTabBarTransitionBugInIPhoneX : 是否需要自动修复 iOS 11 下,iPhone X 的设备在 push 界面时,tabBar 会瞬间往上跳的 bug - QMUICMI.shouldFixTabBarButtonBugForAll = NO; // ShouldFixTabBarButtonBugForAll : 是否要对 iOS 12.1.1 及以后的版本也修复手势返回时 tabBarButton 布局错误的 bug(issue #410),默认为 NO - QMUICMI.shouldPrintQMUIWarnLogToConsole = YES; // ShouldPrintQMUIWarnLogToConsole : 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上 - QMUICMI.sendAnalyticsToQMUITeam = YES; // SendAnalyticsToQMUITeam : 是否允许在 DEBUG 模式下上报 Bundle Identifier 和 Display Name 给 QMUI 统计用 -} - // QMUI 2.3.0 版本里,配置表新增这个方法,返回 YES 表示在 App 启动时要自动应用这份配置表。仅当你的 App 里存在多份配置表时,才需要把除默认配置表之外的其他配置表的返回值改为 NO。 - (BOOL)shouldApplyTemplateAutomatically { - BOOL result = [[[NSUserDefaults standardUserDefaults] stringForKey:QDSelectedThemeClassName] isEqualToString:NSStringFromClass(self.class)]; + [QMUIThemeManagerCenter.defaultThemeManager addThemeIdentifier:self.themeName theme:self]; + + NSString *selectedThemeIdentifier = [[NSUserDefaults standardUserDefaults] stringForKey:QDSelectedThemeIdentifier]; + BOOL result = [selectedThemeIdentifier isEqualToString:self.themeName]; if (result) { - [QDThemeManager sharedInstance].currentTheme = self; + QMUIThemeManagerCenter.defaultThemeManager.currentTheme = self; } return result; } @@ -229,20 +30,8 @@ - (UIColor *)themeTintColor { return UIColorTheme4; } -- (UIColor *)themeListTextColor { - return self.themeTintColor; -} - -- (UIColor *)themeCodeColor { - return self.themeTintColor; -} - -- (UIColor *)themeGridItemTintColor { - return self.themeTintColor; -} - - (NSString *)themeName { - return @"Grass"; + return QDThemeIdentifierGrass; } @end diff --git a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplatePinkRose.h b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplatePinkRose.h index 71c568e2..0134c5d6 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplatePinkRose.h +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplatePinkRose.h @@ -5,17 +5,8 @@ // Copyright (c) 2015年 QMUI Team. All rights reserved. // -#import -#import "QDThemeProtocol.h" +#import "QMUIConfigurationTemplate.h" -/** - * QMUIConfigurationTemplate 是一份配置表,用于配合 QMUIConfiguration 来管理整个 App 的全局样式,使用方式: - * 在 QMUI 项目代码的文件夹里找到 QMUIConfigurationTemplate 目录,把里面所有文件复制到自己项目里,保证能被编译到即可,不需要在某些地方 import,也不需要手动运行。 - * - * @warning 更新 QMUIKit 的版本时,请留意 Release Log 里是否有提醒更新配置表,请尽量保持自己项目里的配置表与 QMUIKit 里的配置表一致,避免遗漏新的属性。 - * @warning 配置表的 class 名必须以 QMUIConfigurationTemplate 开头,并且实现 ,因为这两者是 QMUI 识别该 NSObject 是否为一份配置表的条件。 - * @warning QMUI 2.3.0 之后,配置表改为自动运行,不需要再在某个地方手动运行了。 - */ -@interface QMUIConfigurationTemplatePinkRose : NSObject +@interface QMUIConfigurationTemplatePinkRose : QMUIConfigurationTemplate @end diff --git a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplatePinkRose.m b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplatePinkRose.m index 6e50f547..04193dd8 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplatePinkRose.m +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplatePinkRose.m @@ -7,218 +7,19 @@ // #import "QMUIConfigurationTemplatePinkRose.h" -#import @implementation QMUIConfigurationTemplatePinkRose #pragma mark - -- (void)applyConfigurationTemplate { - - // === 修改配置值 === // - -#pragma mark - Global Color - - QMUICMI.clearColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0]; // UIColorClear : 透明色 - QMUICMI.whiteColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:1]; // UIColorWhite : 白色(不用 [UIColor whiteColor] 是希望保持颜色空间为 RGB) - QMUICMI.blackColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:1]; // UIColorBlack : 黑色(不用 [UIColor blackColor] 是希望保持颜色空间为 RGB) - QMUICMI.grayColor = UIColorGray4; // UIColorGray : 最常用的灰色 - QMUICMI.grayDarkenColor = UIColorGray3; // UIColorGrayDarken : 深一点的灰色 - QMUICMI.grayLightenColor = UIColorGray7; // UIColorGrayLighten : 浅一点的灰色 - QMUICMI.redColor = UIColorMake(250, 58, 58); // UIColorRed : 红色 - QMUICMI.greenColor = UIColorTheme4; // UIColorGreen : 绿色 - QMUICMI.blueColor = UIColorMake(49, 189, 243); // UIColorBlue : 蓝色 - QMUICMI.yellowColor = UIColorTheme3; // UIColorYellow : 黄色 - - QMUICMI.linkColor = UIColorMake(56, 116, 171); // UIColorLink : 文字链接颜色 - QMUICMI.disabledColor = UIColorGray; // UIColorDisabled : 全局 disabled 的颜色,一般用于 UIControl 等控件 - QMUICMI.backgroundColor = UIColorWhite; // UIColorForBackground : 界面背景色,默认用于 QMUICommonViewController.view 的背景色 - QMUICMI.maskDarkColor = UIColorMakeWithRGBA(0, 0, 0, .35f); // UIColorMask : 深色的背景遮罩,默认用于 QMAlertController、QMUIDialogViewController 等弹出控件的遮罩 - QMUICMI.maskLightColor = UIColorMakeWithRGBA(255, 255, 255, .5f); // UIColorMaskWhite : 浅色的背景遮罩,QMUIKit 里默认没用到,只是占个位 - QMUICMI.separatorColor = UIColorMake(222, 224, 226); // UIColorSeparator : 全局默认的分割线颜色,默认用于列表分隔线颜色、UIView (QMUI_Border) 分隔线颜色 - QMUICMI.separatorDashedColor = UIColorMake(17, 17, 17); // UIColorSeparatorDashed : 全局默认的虚线分隔线的颜色,默认 QMUIKit 暂时没用到 - QMUICMI.placeholderColor = UIColorGray8; // UIColorPlaceholder,全局的输入框的 placeholder 颜色,默认用于 QMUITextField、QMUITextView,不影响系统 UIKit 的输入框 - - // 测试用的颜色 - QMUICMI.testColorRed = UIColorMakeWithRGBA(255, 0, 0, .3); - QMUICMI.testColorGreen = UIColorMakeWithRGBA(0, 255, 0, .3); - QMUICMI.testColorBlue = UIColorMakeWithRGBA(0, 0, 255, .3); - - -#pragma mark - UIControl - - QMUICMI.controlHighlightedAlpha = 0.5f; // UIControlHighlightedAlpha : UIControl 系列控件在 highlighted 时的 alpha,默认用于 QMUIButton、 QMUINavigationTitleView - QMUICMI.controlDisabledAlpha = 0.5f; // UIControlDisabledAlpha : UIControl 系列控件在 disabled 时的 alpha,默认用于 QMUIButton - -#pragma mark - UIButton - QMUICMI.buttonHighlightedAlpha = UIControlHighlightedAlpha; // ButtonHighlightedAlpha : QMUIButton 在 highlighted 时的 alpha,不影响系统的 UIButton - QMUICMI.buttonDisabledAlpha = UIControlDisabledAlpha; // ButtonDisabledAlpha : QMUIButton 在 disabled 时的 alpha,不影响系统的 UIButton - QMUICMI.buttonTintColor = self.themeTintColor; // ButtonTintColor : QMUIButton 默认的 tintColor,不影响系统的 UIButton - - QMUICMI.ghostButtonColorBlue = UIColorBlue; // GhostButtonColorBlue : QMUIGhostButtonColorBlue 的颜色 - QMUICMI.ghostButtonColorRed = UIColorRed; // GhostButtonColorRed : QMUIGhostButtonColorRed 的颜色 - QMUICMI.ghostButtonColorGreen = UIColorGreen; // GhostButtonColorGreen : QMUIGhostButtonColorGreen 的颜色 - QMUICMI.ghostButtonColorGray = UIColorGray; // GhostButtonColorGray : QMUIGhostButtonColorGray 的颜色 - QMUICMI.ghostButtonColorWhite = UIColorWhite; // GhostButtonColorWhite : QMUIGhostButtonColorWhite 的颜色 - - QMUICMI.fillButtonColorBlue = UIColorBlue; // FillButtonColorBlue : QMUIFillButtonColorBlue 的颜色 - QMUICMI.fillButtonColorRed = UIColorRed; // FillButtonColorRed : QMUIFillButtonColorRed 的颜色 - QMUICMI.fillButtonColorGreen = UIColorGreen; // FillButtonColorGreen : QMUIFillButtonColorGreen 的颜色 - QMUICMI.fillButtonColorGray = UIColorGray; // FillButtonColorGray : QMUIFillButtonColorGray 的颜色 - QMUICMI.fillButtonColorWhite = UIColorWhite; // FillButtonColorWhite : QMUIFillButtonColorWhite 的颜色 - - -#pragma mark - TextField & TextView - QMUICMI.textFieldTintColor = self.themeTintColor; // TextFieldTintColor : QMUITextField、QMUITextView 的 tintColor,不影响 UIKit 的输入框 - QMUICMI.textFieldTextInsets = UIEdgeInsetsMake(0, 7, 0, 7); // TextFieldTextInsets : QMUITextField 的内边距,不影响 UITextField - -#pragma mark - NavigationBar - - QMUICMI.navBarHighlightedAlpha = 0.2f; // NavBarHighlightedAlpha : QMUINavigationButton 在 highlighted 时的 alpha - QMUICMI.navBarDisabledAlpha = 0.2f; // NavBarDisabledAlpha : QMUINavigationButton 在 disabled 时的 alpha - QMUICMI.navBarButtonFont = UIFontMake(17); // NavBarButtonFont : QMUINavigationButtonTypeNormal 的字体(由于系统存在一些 bug,这个属性默认不对 UIBarButtonItem 生效) - QMUICMI.navBarButtonFontBold = UIFontBoldMake(17); // NavBarButtonFontBold : QMUINavigationButtonTypeBold 的字体 - QMUICMI.navBarBackgroundImage = [QDUIHelper navigationBarBackgroundImageWithThemeColor:self.themeTintColor]; // NavBarBackgroundImage : UINavigationBar 的背景图,注意 navigationBar 的高度会受多个因素(是否全面屏、是否使用了 navigationItem.prompt、是否将 UISearchBar 作为 titleView)的影响,要检查各种情况是否都显示正常。 - QMUICMI.navBarShadowImage = [UIImage qmui_imageWithColor:UIColorClear size:CGSizeMake(4, PixelOne) cornerRadius:0]; // NavBarShadowImage : UINavigationBar.shadowImage,也即导航栏底部那条分隔线 - QMUICMI.navBarBarTintColor = nil; // NavBarBarTintColor : UINavigationBar.barTintColor,也即背景色 - QMUICMI.navBarTintColor = UIColorWhite; // NavBarTintColor : UINavigationBar 的 tintColor,也即导航栏上面的按钮颜色 - QMUICMI.navBarTitleColor = NavBarTintColor; // NavBarTitleColor : UINavigationBar 的标题颜色,以及 QMUINavigationTitleView 的默认文字颜色 - QMUICMI.navBarTitleFont = UIFontBoldMake(17); // NavBarTitleFont : UINavigationBar 的标题字体,以及 QMUINavigationTitleView 的默认字体 - QMUICMI.navBarLargeTitleColor = nil; // NavBarLargeTitleColor : UINavigationBar 在大标题模式下的标题颜色,仅在 iOS 11 之后才有效 - QMUICMI.navBarLargeTitleFont = nil; // NavBarLargeTitleFont : UINavigationBar 在大标题模式下的标题字体,仅在 iOS 11 之后才有效 - QMUICMI.navBarBackButtonTitlePositionAdjustment = UIOffsetZero; // NavBarBarBackButtonTitlePositionAdjustment : 导航栏返回按钮的文字偏移 - QMUICMI.sizeNavBarBackIndicatorImageAutomatically = YES; // SizeNavBarBackIndicatorImageAutomatically : 是否要自动调整 NavBarBackIndicatorImage 的 size 为 (13, 21) - QMUICMI.navBarBackIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavBack size:CGSizeMake(12, 20) tintColor:NavBarTintColor]; // NavBarBackIndicatorImage : 导航栏的返回按钮的图片,图片尺寸建议为(13, 21),否则最终的图片位置无法与系统原生的位置保持一致 - QMUICMI.navBarCloseButtonImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavClose size:CGSizeMake(16, 16) tintColor:NavBarTintColor]; // NavBarCloseButtonImage : QMUINavigationButton 用到的 × 的按钮图片 - - QMUICMI.navBarLoadingMarginRight = 3; // NavBarLoadingMarginRight : QMUINavigationTitleView 里左边 loading 的右边距 - QMUICMI.navBarAccessoryViewMarginLeft = 5; // NavBarAccessoryViewMarginLeft : QMUINavigationTitleView 里右边 accessoryView 的左边距 - QMUICMI.navBarActivityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;// NavBarActivityIndicatorViewStyle : QMUINavigationTitleView 里左边 loading 的主题 - QMUICMI.navBarAccessoryViewTypeDisclosureIndicatorImage = [[UIImage qmui_imageWithShape:QMUIImageShapeTriangle size:CGSizeMake(8, 5) tintColor:nil] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; // NavBarAccessoryViewTypeDisclosureIndicatorImage : QMUINavigationTitleView 右边箭头的图片 - -#pragma mark - TabBar - - QMUICMI.tabBarBackgroundImage = [[UIImage qmui_imageWithColor:UIColorMake(249, 249, 249)] resizableImageWithCapInsets:UIEdgeInsetsMake(1, 1, 1, 1)]; // TabBarBackgroundImage : UITabBar 的背景图,建议使用 resizableImage,否则在 UITabBar (NavigationController) 的 setBackgroundImage: 里会每次都视为 image 发生了变化(isEqual: 为 NO) - QMUICMI.tabBarBarTintColor = nil; // TabBarBarTintColor : UITabBar 的 barTintColor - QMUICMI.tabBarShadowImageColor = UIColorSeparator; // TabBarShadowImageColor : UITabBar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片 - QMUICMI.tabBarTintColor = self.themeTintColor; // TabBarTintColor : UITabBar 的 tintColor - QMUICMI.tabBarItemTitleColor = UIColorGray6; // TabBarItemTitleColor : 未选中的 UITabBarItem 的标题颜色 - QMUICMI.tabBarItemTitleColorSelected = TabBarTintColor; // TabBarItemTitleColorSelected : 选中的 UITabBarItem 的标题颜色 - QMUICMI.tabBarItemTitleFont = nil; // TabBarItemTitleFont : UITabBarItem 的标题字体 - -#pragma mark - Toolbar - - QMUICMI.toolBarHighlightedAlpha = 0.4f; // ToolBarHighlightedAlpha : QMUIToolbarButton 在 highlighted 状态下的 alpha - QMUICMI.toolBarDisabledAlpha = 0.4f; // ToolBarDisabledAlpha : QMUIToolbarButton 在 disabled 状态下的 alpha - QMUICMI.toolBarTintColor = UIColorBlue; // ToolBarTintColor : UIToolbar 的 tintColor,以及 QMUIToolbarButton normal 状态下的文字颜色 - QMUICMI.toolBarTintColorHighlighted = [ToolBarTintColor colorWithAlphaComponent:ToolBarHighlightedAlpha]; // ToolBarTintColorHighlighted : QMUIToolbarButton 在 highlighted 状态下的文字颜色 - QMUICMI.toolBarTintColorDisabled = [ToolBarTintColor colorWithAlphaComponent:ToolBarDisabledAlpha]; // ToolBarTintColorDisabled : QMUIToolbarButton 在 disabled 状态下的文字颜色 - QMUICMI.toolBarBackgroundImage = nil; // ToolBarBackgroundImage : UIToolbar 的背景图 - QMUICMI.toolBarBarTintColor = nil; // ToolBarBarTintColor : UIToolbar 的 tintColor - QMUICMI.toolBarShadowImageColor = UIColorSeparator; // ToolBarShadowImageColor : UIToolbar 的 shadowImage 的颜色,会自动创建一张 1px 高的图片 - QMUICMI.toolBarButtonFont = UIFontMake(17); // ToolBarButtonFont : QMUIToolbarButton 的字体 - -#pragma mark - SearchBar - - QMUICMI.searchBarTextFieldBackground = UIColorMake(237, 238, 240); // SearchBarTextFieldBackground : QMUISearchBar 里的文本框的背景颜色 - QMUICMI.searchBarTextFieldBorderColor = nil; // SearchBarTextFieldBorderColor : QMUISearchBar 里的文本框的边框颜色 - QMUICMI.searchBarBottomBorderColor = nil; // SearchBarBottomBorderColor : QMUISearchBar 底部分隔线颜色 - QMUICMI.searchBarBarTintColor = UIColorWhite; // SearchBarBarTintColor : QMUISearchBar 的 barTintColor,也即背景色 - QMUICMI.searchBarTintColor = self.themeTintColor; // SearchBarTintColor : QMUISearchBar 的 tintColor,也即上面的操作控件的主题色 - QMUICMI.searchBarTextColor = UIColorBlack; // SearchBarTextColor : QMUISearchBar 里的文本框的文字颜色 - QMUICMI.searchBarPlaceholderColor = UIColorMake(136, 136, 143); // SearchBarPlaceholderColor : QMUISearchBar 里的文本框的 placeholder 颜色 - QMUICMI.searchBarFont = nil; // SearchBarFont : QMUISearchBar 里的文本框的文字字体及 placeholder 的字体 - QMUICMI.searchBarSearchIconImage = nil; // SearchBarSearchIconImage : QMUISearchBar 里的放大镜 icon - QMUICMI.searchBarClearIconImage = nil; // SearchBarClearIconImage : QMUISearchBar 里的文本框输入文字时右边的清空按钮的图片 - QMUICMI.searchBarTextFieldCornerRadius = 4.0; // SearchBarTextFieldCornerRadius : QMUISearchBar 里的文本框的圆角大小,-1 表示圆角大小为输入框高度的一半 - -#pragma mark - TableView / TableViewCell - - QMUICMI.tableViewEstimatedHeightEnabled = YES; // TableViewEstimatedHeightEnabled : 是否要开启全局 UITableView 的 estimatedRow(Section/Footer)Height - - QMUICMI.tableViewBackgroundColor = nil; // TableViewBackgroundColor : Plain 类型的 QMUITableView 的背景色颜色 - QMUICMI.tableViewGroupedBackgroundColor = UIColorMake(246, 246, 246); // TableViewGroupedBackgroundColor : Grouped 类型的 QMUITableView 的背景色 - QMUICMI.tableSectionIndexColor = UIColorGrayDarken; // TableSectionIndexColor : 列表右边的字母索引条的文字颜色 - QMUICMI.tableSectionIndexBackgroundColor = UIColorClear; // TableSectionIndexBackgroundColor : 列表右边的字母索引条的背景色 - QMUICMI.tableSectionIndexTrackingBackgroundColor = UIColorClear; // TableSectionIndexTrackingBackgroundColor : 列表右边的字母索引条在选中时的背景色 - QMUICMI.tableViewSeparatorColor = UIColorSeparator; // TableViewSeparatorColor : 列表的分隔线颜色 - - QMUICMI.tableViewCellNormalHeight = 56; // TableViewCellNormalHeight : QMUITableView 的默认 cell 高度 - QMUICMI.tableViewCellTitleLabelColor = UIColorGray3; // TableViewCellTitleLabelColor : QMUITableViewCell 的 textLabel 的文字颜色 - QMUICMI.tableViewCellDetailLabelColor = UIColorGray5; // TableViewCellDetailLabelColor : QMUITableViewCell 的 detailTextLabel 的文字颜色 - QMUICMI.tableViewCellBackgroundColor = UIColorWhite; // TableViewCellBackgroundColor : QMUITableViewCell 的背景色 - QMUICMI.tableViewCellSelectedBackgroundColor = UIColorMake(238, 239, 241); // TableViewCellSelectedBackgroundColor : QMUITableViewCell 点击时的背景色 - QMUICMI.tableViewCellWarningBackgroundColor = UIColorYellow; // TableViewCellWarningBackgroundColor : QMUITableViewCell 用于表示警告时的背景色,备用 - QMUICMI.tableViewCellDisclosureIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeDisclosureIndicator size:CGSizeMake(6, 10) lineWidth:1 tintColor:UIColorMake(173, 180, 190)]; // TableViewCellDisclosureIndicatorImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryDisclosureIndicator 时的箭头的图片 - QMUICMI.tableViewCellCheckmarkImage = [UIImage qmui_imageWithShape:QMUIImageShapeCheckmark size:CGSizeMake(15, 12) tintColor:self.themeTintColor]; // TableViewCellCheckmarkImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryCheckmark 时的打钩的图片 - QMUICMI.tableViewCellDetailButtonImage = [UIImage qmui_imageWithShape:QMUIImageShapeDetailButtonImage size:CGSizeMake(20, 20) tintColor:self.themeTintColor]; // TableViewCellDetailButtonImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryDetailButton 或 UITableViewCellAccessoryDetailDisclosureButton 时右边的 i 按钮图片 - QMUICMI.tableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator = 12; // TableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator : 列表 cell 右边的 i 按钮和向右箭头之间的间距(仅当两者都使用了自定义图片并且同时显示时才生效) - - QMUICMI.tableViewSectionHeaderBackgroundColor = UIColorMake(244, 244, 244); // TableViewSectionHeaderBackgroundColor : Plain 类型的 QMUITableView sectionHeader 的背景色 - QMUICMI.tableViewSectionFooterBackgroundColor = UIColorMake(244, 244, 244); // TableViewSectionFooterBackgroundColor : Plain 类型的 QMUITableView sectionFooter 的背景色 - QMUICMI.tableViewSectionHeaderFont = UIFontBoldMake(12); // TableViewSectionHeaderFont : Plain 类型的 QMUITableView sectionHeader 里的文字字体 - QMUICMI.tableViewSectionFooterFont = UIFontBoldMake(12); // TableViewSectionFooterFont : Plain 类型的 QMUITableView sectionFooter 里的文字字体 - QMUICMI.tableViewSectionHeaderTextColor = UIColorGray5; // TableViewSectionHeaderTextColor : Plain 类型的 QMUITableView sectionHeader 里的文字颜色 - QMUICMI.tableViewSectionFooterTextColor = UIColorGray; // TableViewSectionFooterTextColor : Plain 类型的 QMUITableView sectionFooter 里的文字颜色 - QMUICMI.tableViewSectionHeaderAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewSectionHeaderAccessoryMargins : Plain 类型的 QMUITableView sectionHeader accessoryView 的间距 - QMUICMI.tableViewSectionFooterAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewSectionFooterAccessoryMargins : Plain 类型的 QMUITableView sectionFooter accessoryView 的间距 - QMUICMI.tableViewSectionHeaderContentInset = UIEdgeInsetsMake(4, 15, 4, 15); // TableViewSectionHeaderContentInset : Plain 类型的 QMUITableView sectionHeader 里的内容的 padding - QMUICMI.tableViewSectionFooterContentInset = UIEdgeInsetsMake(4, 15, 4, 15); // TableViewSectionFooterContentInset : Plain 类型的 QMUITableView sectionFooter 里的内容的 padding - - QMUICMI.tableViewGroupedSectionHeaderFont = UIFontMake(12); // TableViewGroupedSectionHeaderFont : Grouped 类型的 QMUITableView sectionHeader 里的文字字体 - QMUICMI.tableViewGroupedSectionFooterFont = UIFontMake(12); // TableViewGroupedSectionFooterFont : Grouped 类型的 QMUITableView sectionFooter 里的文字字体 - QMUICMI.tableViewGroupedSectionHeaderTextColor = UIColorGrayDarken; // TableViewGroupedSectionHeaderTextColor : Grouped 类型的 QMUITableView sectionHeader 里的文字颜色 - QMUICMI.tableViewGroupedSectionFooterTextColor = UIColorGray; // TableViewGroupedSectionFooterTextColor : Grouped 类型的 QMUITableView sectionFooter 里的文字颜色 - QMUICMI.tableViewGroupedSectionHeaderAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewGroupedSectionHeaderAccessoryMargins : Grouped 类型的 QMUITableView sectionHeader accessoryView 的间距 - QMUICMI.tableViewGroupedSectionFooterAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewGroupedSectionFooterAccessoryMargins : Grouped 类型的 QMUITableView sectionFooter accessoryView 的间距 - QMUICMI.tableViewGroupedSectionHeaderDefaultHeight = UITableViewAutomaticDimension; // TableViewGroupedSectionHeaderDefaultHeight : Grouped 类型的 QMUITableView sectionHeader 的默认高度(也即没使用自定义的 sectionHeaderView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN - QMUICMI.tableViewGroupedSectionFooterDefaultHeight = UITableViewAutomaticDimension; // TableViewGroupedSectionFooterDefaultHeight : Grouped 类型的 QMUITableView sectionFooter 的默认高度(也即没使用自定义的 sectionFooterView 时的高度),注意如果不需要间距,请用 CGFLOAT_MIN - QMUICMI.tableViewGroupedSectionHeaderContentInset = UIEdgeInsetsMake(16, PreferredValueForiPhone(20, 15, 15, 15), 8, PreferredValueForiPhone(20, 15, 15, 15)); // TableViewGroupedSectionHeaderContentInset : Grouped 类型的 QMUITableView sectionHeader 里的内容的 padding - QMUICMI.tableViewGroupedSectionFooterContentInset = UIEdgeInsetsMake(8, 15, 2, 15); // TableViewGroupedSectionFooterContentInset : Grouped 类型的 QMUITableView sectionFooter 里的内容的 padding - -#pragma mark - UIWindowLevel - QMUICMI.windowLevelQMUIAlertView = UIWindowLevelAlert - 4.0; // UIWindowLevelQMUIAlertView : QMUIModalPresentationViewController、QMUIPopupContainerView 里使用的 UIWindow 的 windowLevel - -#pragma mark - QMUILog - QMUICMI.shouldPrintDefaultLog = YES; // ShouldPrintDefaultLog : 是否允许输出 QMUILogLevelDefault 级别的 log - QMUICMI.shouldPrintInfoLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelInfo 级别的 log - QMUICMI.shouldPrintWarnLog = YES; // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelWarn 级别的 log - -#pragma mark - QMUIBadge - - QMUICMI.badgeBackgroundColor = UIColorRed; // BadgeBackgroundColor : UIBarButtoItem、UITabBarItem 上的未读数的背景色 - QMUICMI.badgeTextColor = UIColorWhite; // BadgeTextColor : UIBarButtoItem、UITabBarItem 上的未读数的文字颜色 - QMUICMI.badgeFont = UIFontBoldMake(11); // BadgeFont : UIBarButtoItem、UITabBarItem 上的未读数的字体 - QMUICMI.badgeContentEdgeInsets = UIEdgeInsetsMake(2, 4, 2, 4); // BadgeContentEdgeInsets : UIBarButtoItem、UITabBarItem 上的未读数与圆圈之间的 padding - QMUICMI.badgeCenterOffset = CGPointMake(14, -10); // BadgeCenterOffset : UIBarButtoItem、UITabBarItem 上的未读数相对于 item 中心的偏移 - QMUICMI.badgeCenterOffsetLandscape = CGPointMake(16, -7); // BadgeCenterOffsetLandscape : UIBarButtoItem、UITabBarItem 上的未读数在横屏下相对于 item 中心的偏移 - - QMUICMI.updatesIndicatorColor = UIColorRed; // UpdatesIndicatorColor : UIBarButtoItem、UITabBarItem 上的未读红点的颜色 - QMUICMI.updatesIndicatorSize = CGSizeMake(7, 7); // UpdatesIndicatorSize : UIBarButtoItem、UITabBarItem 上的未读红点的大小 - QMUICMI.updatesIndicatorCenterOffset = CGPointMake(14, -10); // UpdatesIndicatorCenterOffset : UIBarButtoItem、UITabBarItem 上的未读红点相对于 item 中心的偏移 - QMUICMI.updatesIndicatorCenterOffsetLandscape = CGPointMake(14, -10); // UpdatesIndicatorCenterOffsetLandscape : UIBarButtoItem、UITabBarItem 上的未读红点在横屏下相对于 item 中心的偏移 - -#pragma mark - Others - - QMUICMI.automaticCustomNavigationBarTransitionStyle = NO; // AutomaticCustomNavigationBarTransitionStyle : 界面 push/pop 时是否要自动根据两个界面的 barTintColor/backgroundImage/shadowImage 的样式差异来决定是否使用自定义的导航栏效果 - QMUICMI.supportedOrientationMask = UIInterfaceOrientationMaskAll; // SupportedOrientationMask : 默认支持的横竖屏方向 - QMUICMI.automaticallyRotateDeviceOrientation = YES; // AutomaticallyRotateDeviceOrientation : 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕 - QMUICMI.statusbarStyleLightInitially = YES; // StatusbarStyleLightInitially : 默认的状态栏内容是否使用白色,默认为 NO,也即黑色 - QMUICMI.needsBackBarButtonItemTitle = NO; // NeedsBackBarButtonItemTitle : 全局是否需要返回按钮的 title,不需要则只显示一个返回image - QMUICMI.hidesBottomBarWhenPushedInitially = YES; // HidesBottomBarWhenPushedInitially : QMUICommonViewController.hidesBottomBarWhenPushed 的初始值,默认为 NO,以保持与系统默认值一致,但通常建议改为 YES,因为一般只有 tabBar 首页那几个界面要求为 NO - QMUICMI.preventConcurrentNavigationControllerTransitions = YES; // PreventConcurrentNavigationControllerTransitions : 自动保护 QMUINavigationController 在上一次 push/pop 尚未结束的时候就进行下一次 push/pop 的行为,避免产生 crash - QMUICMI.navigationBarHiddenInitially = NO; // NavigationBarHiddenInitially : QMUINavigationControllerDelegate preferredNavigationBarHidden 的初始值,默认为NO - QMUICMI.shouldFixTabBarTransitionBugInIPhoneX = YES; // ShouldFixTabBarTransitionBugInIPhoneX : 是否需要自动修复 iOS 11 下,iPhone X 的设备在 push 界面时,tabBar 会瞬间往上跳的 bug - QMUICMI.shouldFixTabBarButtonBugForAll = NO; // ShouldFixTabBarButtonBugForAll : 是否要对 iOS 12.1.1 及以后的版本也修复手势返回时 tabBarButton 布局错误的 bug(issue #410),默认为 NO - QMUICMI.shouldPrintQMUIWarnLogToConsole = YES; // ShouldPrintQMUIWarnLogToConsole : 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上 - QMUICMI.sendAnalyticsToQMUITeam = YES; // SendAnalyticsToQMUITeam : 是否允许在 DEBUG 模式下上报 Bundle Identifier 和 Display Name 给 QMUI 统计用 -} - // QMUI 2.3.0 版本里,配置表新增这个方法,返回 YES 表示在 App 启动时要自动应用这份配置表。仅当你的 App 里存在多份配置表时,才需要把除默认配置表之外的其他配置表的返回值改为 NO。 - (BOOL)shouldApplyTemplateAutomatically { - BOOL result = [[[NSUserDefaults standardUserDefaults] stringForKey:QDSelectedThemeClassName] isEqualToString:NSStringFromClass(self.class)]; + [QMUIThemeManagerCenter.defaultThemeManager addThemeIdentifier:self.themeName theme:self]; + + NSString *selectedThemeIdentifier = [[NSUserDefaults standardUserDefaults] stringForKey:QDSelectedThemeIdentifier]; + BOOL result = [selectedThemeIdentifier isEqualToString:self.themeName]; if (result) { - [QDThemeManager sharedInstance].currentTheme = self; + QMUIThemeManagerCenter.defaultThemeManager.currentTheme = self; } return result; } @@ -229,20 +30,8 @@ - (UIColor *)themeTintColor { return UIColorTheme9; } -- (UIColor *)themeListTextColor { - return self.themeTintColor; -} - -- (UIColor *)themeCodeColor { - return self.themeTintColor; -} - -- (UIColor *)themeGridItemTintColor { - return self.themeTintColor; -} - - (NSString *)themeName { - return @"Pink Rose"; + return QDThemeIdentifierPinkRose; } @end diff --git a/qmuidemo/Modules/Common/Controllers/QDCommonGridViewController.m b/qmuidemo/Modules/Common/Controllers/QDCommonGridViewController.m index 289112d7..5d4d8302 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonGridViewController.m +++ b/qmuidemo/Modules/Common/Controllers/QDCommonGridViewController.m @@ -22,7 +22,7 @@ - (void)didInitialize { - (void)initSubviews { [super initSubviews]; - _scrollView = [[UIScrollView alloc] init]; + _scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; [self.view addSubview:self.scrollView]; _gridView = [[QMUIGridView alloc] init]; @@ -36,14 +36,14 @@ - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; self.scrollView.frame = self.view.bounds; - CGFloat gridViewWidth = CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(self.scrollView.qmui_safeAreaInsets); + CGFloat gridViewWidth = CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(self.scrollView.safeAreaInsets); - if (CGRectGetWidth(self.view.bounds) <= [QMUIHelper screenSizeFor55Inch].width) { + if (CGRectGetWidth(self.view.bounds) <= [QMUIHelper screenSizeFor69Inch].width) { self.gridView.columnCount = 3; CGFloat itemWidth = flat(gridViewWidth / self.gridView.columnCount); self.gridView.rowHeight = itemWidth; } else { - CGFloat minimumItemWidth = flat([QMUIHelper screenSizeFor55Inch].width / 3.0); + CGFloat minimumItemWidth = flat([QMUIHelper screenSizeFor69Inch].width / 3.0); CGFloat maximumItemWidth = flat(gridViewWidth / 5.0); CGFloat freeSpacingWhenDisplayingMinimumCount = gridViewWidth / maximumItemWidth - floor(gridViewWidth / maximumItemWidth); CGFloat freeSpacingWhenDisplayingMaximumCount = gridViewWidth / minimumItemWidth - floor(gridViewWidth / minimumItemWidth); @@ -72,52 +72,30 @@ - (void)viewDidLayoutSubviews { } } - self.gridView.frame = CGRectMake(self.scrollView.qmui_safeAreaInsets.left, 0, gridViewWidth, QMUIViewSelfSizingHeight); + self.gridView.frame = CGRectMake(self.scrollView.safeAreaInsets.left, 0, gridViewWidth, QMUIViewSelfSizingHeight); self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.gridView.frame), CGRectGetMaxY(self.gridView.frame)); } - (QDCommonGridButton *)generateButtonAtIndex:(NSInteger)index { NSString *keyName = self.dataSource.allKeys[index]; - NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:keyName attributes:@{NSForegroundColorAttributeName: UIColorGray6, NSFontAttributeName: UIFontMake(11), NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:12 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]}]; - UIImage *image = (UIImage *)[self.dataSource objectForKey:keyName]; + NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:keyName attributes:@{NSForegroundColorAttributeName: UIColor.qd_descriptionTextColor, NSFontAttributeName: UIFontMake(11), NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:12 lineBreakMode:NSLineBreakByTruncatingTail textAlignment:NSTextAlignmentCenter]}]; + UIImage *image = (UIImage *)self.dataSource[keyName]; + image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; QDCommonGridButton *button = [[QDCommonGridButton alloc] init]; - UIColor *tintColor = [QDThemeManager sharedInstance].currentTheme.themeGridItemTintColor; - if (tintColor) { - button.tintColor = tintColor; - button.adjustsImageTintColorAutomatically = YES; - } else { - button.tintColor = nil; - button.adjustsImageTintColorAutomatically = NO; - } + button.tintColor = UIColor.qd_gridItemTintColor; [button setAttributedTitle:attributedString forState:UIControlStateNormal]; [button setImage:image forState:UIControlStateNormal]; button.tag = index; - [button addTarget:self action:@selector(handleGirdButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [button addTarget:self action:@selector(handleGridButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; return button; } -- (void)handleGirdButtonEvent:(QDCommonGridButton *)button { +- (void)handleGridButtonEvent:(QDCommonGridButton *)button { NSString *keyName = self.dataSource.allKeys[button.tag]; [self didSelectCellWithTitle:keyName]; } -#pragma mark - - -- (void)themeBeforeChanged:(NSObject *)themeBeforeChanged afterChanged:(NSObject *)themeAfterChanged { - [super themeBeforeChanged:themeBeforeChanged afterChanged:themeAfterChanged]; - for (QDCommonGridButton *button in self.gridView.subviews) { - UIColor *tintColor = themeAfterChanged.themeGridItemTintColor; - if (tintColor) { - button.tintColor = tintColor; - button.adjustsImageTintColorAutomatically = YES; - } else { - button.tintColor = nil; - button.adjustsImageTintColorAutomatically = NO; - } - } -} - @end @implementation QDCommonGridViewController (UISubclassingHooks) @@ -159,7 +137,7 @@ - (void)layoutSubviews { self.imageView.center = CGPointMake(center.x, center.y - 12); - self.titleLabel.frame = CGRectFlatMake(self.contentEdgeInsets.left, center.y + PreferredValueForiPhone(27, 27, 21, 21), contentSize.width, QMUIViewSelfSizingHeight); + self.titleLabel.frame = CGRectFlatMake(self.contentEdgeInsets.left, center.y + 27, contentSize.width, QMUIViewSelfSizingHeight); } @end diff --git a/qmuidemo/Modules/Common/Controllers/QDCommonListViewController.h b/qmuidemo/Modules/Common/Controllers/QDCommonListViewController.h index 4a00704f..fb7022fb 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonListViewController.h +++ b/qmuidemo/Modules/Common/Controllers/QDCommonListViewController.h @@ -8,10 +8,14 @@ #import "QDCommonTableViewController.h" +/** + 对于那种简单地展示一个列表,点击后跳去别的界面的场景,不需要继承,直接初始化后赋值 @c dataSource 、@c dataSourceWithDetailText ,并在 didSelectTitleBlock 里处理每一行的点击事件即可。 + */ @interface QDCommonListViewController : QDCommonTableViewController @property(nonatomic, strong) NSArray *dataSource; @property(nonatomic, strong) QMUIOrderedDictionary *dataSourceWithDetailText; +@property(nonatomic, copy) void (^didSelectTitleBlock)(NSString *title); @end diff --git a/qmuidemo/Modules/Common/Controllers/QDCommonListViewController.m b/qmuidemo/Modules/Common/Controllers/QDCommonListViewController.m index 478101a5..7e576f43 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonListViewController.m +++ b/qmuidemo/Modules/Common/Controllers/QDCommonListViewController.m @@ -35,6 +35,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N if (!cell) { if ([identifier isEqualToString:@"subtitle"]) { cell = [[QMUITableViewCell alloc] initForTableView:tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier]; + cell.detailTextLabel.numberOfLines = 0; } else { cell = [[QMUITableViewCell alloc] initForTableView:tableView withStyle:UITableViewCellStyleValue1 reuseIdentifier:identifier]; } @@ -55,7 +56,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if (self.dataSourceWithDetailText && ((NSString *)[self.dataSourceWithDetailText objectForKey:self.dataSourceWithDetailText.allKeys[indexPath.row]]).length) { - return 64; + return UITableViewAutomaticDimension; } return TableViewCellNormalHeight; } @@ -67,6 +68,9 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } else { title = [self.dataSource objectAtIndex:indexPath.row]; } + if (self.didSelectTitleBlock) { + self.didSelectTitleBlock(title); + } [self didSelectCellWithTitle:title]; } diff --git a/qmuidemo/Modules/Common/Controllers/QDCommonTableViewController.h b/qmuidemo/Modules/Common/Controllers/QDCommonTableViewController.h index daf9664d..95ebd944 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonTableViewController.h +++ b/qmuidemo/Modules/Common/Controllers/QDCommonTableViewController.h @@ -6,8 +6,6 @@ // Copyright (c) 2015年 QMUI Team. All rights reserved. // -#import "QDThemeProtocol.h" - -@interface QDCommonTableViewController : QMUICommonTableViewController +@interface QDCommonTableViewController : QMUICommonTableViewController @end diff --git a/qmuidemo/Modules/Common/Controllers/QDCommonTableViewController.m b/qmuidemo/Modules/Common/Controllers/QDCommonTableViewController.m index b6bdc13b..c1463838 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonTableViewController.m +++ b/qmuidemo/Modules/Common/Controllers/QDCommonTableViewController.m @@ -10,28 +10,37 @@ @implementation QDCommonTableViewController -- (void)didInitialize { - [super didInitialize]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleThemeChangedNotification:) name:QDThemeChangedNotification object:nil]; +- (void)initTableView { + [super initTableView]; + if (IsUITest) { + self.tableView.accessibilityLabel = [NSString stringWithFormat:@"viewController-%@", self.title]; + } } -- (void)handleThemeChangedNotification:(NSNotification *)notification { - NSObject *themeBeforeChanged = notification.userInfo[QDThemeBeforeChangedName]; - themeBeforeChanged = [themeBeforeChanged isKindOfClass:[NSNull class]] ? nil : themeBeforeChanged; - - NSObject *themeAfterChanged = notification.userInfo[QDThemeAfterChangedName]; - themeAfterChanged = [themeAfterChanged isKindOfClass:[NSNull class]] ? nil : themeAfterChanged; - - [self themeBeforeChanged:themeBeforeChanged afterChanged:themeAfterChanged]; +- (void)setTitle:(NSString *)title { + [super setTitle:title]; + if (IsUITest && self.isViewLoaded) { + self.tableView.accessibilityLabel = [NSString stringWithFormat:@"viewController-%@", self.title]; + } +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + if (self.qmui_isPresented) { + self.navigationItem.leftBarButtonItem = [UIBarButtonItem qmui_closeItemWithTarget:self action:@selector(handleCloseItem)]; + } +} + +- (void)handleCloseItem { + [self dismissViewControllerAnimated:YES completion:nil]; } - (BOOL)shouldCustomizeNavigationBarTransitionIfHideable { return YES; } -#pragma mark - - -- (void)themeBeforeChanged:(NSObject *)themeBeforeChanged afterChanged:(NSObject *)themeAfterChanged { +- (void)qmui_themeDidChangeByManager:(QMUIThemeManager *)manager identifier:(__kindof NSObject *)identifier theme:(__kindof NSObject *)theme { + [super qmui_themeDidChangeByManager:manager identifier:identifier theme:theme]; [self.tableView reloadData]; } diff --git a/qmuidemo/Modules/Common/Controllers/QDCommonViewController.h b/qmuidemo/Modules/Common/Controllers/QDCommonViewController.h index b5e2a007..b05b8dcb 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonViewController.h +++ b/qmuidemo/Modules/Common/Controllers/QDCommonViewController.h @@ -6,8 +6,6 @@ // Copyright (c) 2015年 QMUI Team. All rights reserved. // -#import "QDThemeProtocol.h" - -@interface QDCommonViewController : QMUICommonViewController +@interface QDCommonViewController : QMUICommonViewController @end diff --git a/qmuidemo/Modules/Common/Controllers/QDCommonViewController.m b/qmuidemo/Modules/Common/Controllers/QDCommonViewController.m index 9f9a479f..ab01ecea 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonViewController.m +++ b/qmuidemo/Modules/Common/Controllers/QDCommonViewController.m @@ -10,24 +10,33 @@ @implementation QDCommonViewController -- (void)didInitialize { - [super didInitialize]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleThemeChangedNotification:) name:QDThemeChangedNotification object:nil]; +- (void)viewDidLoad { + [super viewDidLoad]; + if (IsUITest) { + self.view.accessibilityLabel = [NSString stringWithFormat:@"viewController-%@", self.title]; + } } -- (void)handleThemeChangedNotification:(NSNotification *)notification { - NSObject *themeBeforeChanged = notification.userInfo[QDThemeBeforeChangedName]; - NSObject *themeAfterChanged = notification.userInfo[QDThemeAfterChangedName]; - [self themeBeforeChanged:themeBeforeChanged afterChanged:themeAfterChanged]; +- (void)setTitle:(NSString *)title { + [super setTitle:title]; + if (IsUITest && self.isViewLoaded) { + self.view.accessibilityLabel = [NSString stringWithFormat:@"viewController-%@", self.title]; + } } -- (BOOL)shouldCustomizeNavigationBarTransitionIfHideable { - return YES; +- (void)setupNavigationItems { + [super setupNavigationItems]; + if (self.qmui_isPresented) { + self.navigationItem.leftBarButtonItem = [UIBarButtonItem qmui_closeItemWithTarget:self action:@selector(handleCloseItem)]; + } } -#pragma mark - +- (void)handleCloseItem { + [self dismissViewControllerAnimated:YES completion:nil]; +} -- (void)themeBeforeChanged:(NSObject *)themeBeforeChanged afterChanged:(NSObject *)themeAfterChanged { +- (BOOL)shouldCustomizeNavigationBarTransitionIfHideable { + return YES; } @end diff --git a/qmuidemo/Modules/Common/Controllers/QDStyleSelectableTableViewController.h b/qmuidemo/Modules/Common/Controllers/QDStyleSelectableTableViewController.h new file mode 100644 index 00000000..a07f302b --- /dev/null +++ b/qmuidemo/Modules/Common/Controllers/QDStyleSelectableTableViewController.h @@ -0,0 +1,18 @@ +// +// QDStyleSelectableTableViewController.h +// qmuidemo +// +// Created by MoLice on 2020/7/8. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDCommonTableViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDStyleSelectableTableViewController : QDCommonTableViewController + +@property(nonatomic, strong) UISegmentedControl *segmentedTitleView; +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Common/Controllers/QDStyleSelectableTableViewController.m b/qmuidemo/Modules/Common/Controllers/QDStyleSelectableTableViewController.m new file mode 100644 index 00000000..9df8dda7 --- /dev/null +++ b/qmuidemo/Modules/Common/Controllers/QDStyleSelectableTableViewController.m @@ -0,0 +1,39 @@ +// +// QDStyleSelectableTableViewController.m +// qmuidemo +// +// Created by MoLice on 2020/7/8. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDStyleSelectableTableViewController.h" + +@implementation QDStyleSelectableTableViewController + +- (void)setupNavigationItems { + [super setupNavigationItems]; + if (!self.segmentedTitleView) { + self.segmentedTitleView = [[UISegmentedControl alloc] initWithItems:@[ + @"Plain", + @"Grouped", + @"InsetGrouped" + ]]; + [self.segmentedTitleView addTarget:self action:@selector(handleTableViewStyleChanged:) forControlEvents:UIControlEventValueChanged]; + + UIColor *tintColor = self.navigationController.navigationBar.tintColor; + self.segmentedTitleView.selectedSegmentTintColor = tintColor; + [self.segmentedTitleView setTitleTextAttributes:@{NSForegroundColorAttributeName: tintColor} forState:UIControlStateNormal]; + [self.segmentedTitleView setTitleTextAttributes:@{NSForegroundColorAttributeName: UIColor.qd_tintColor} forState:UIControlStateSelected]; + } + self.segmentedTitleView.selectedSegmentIndex = self.tableView.style; + self.navigationItem.titleView = self.segmentedTitleView; +} + +- (void)handleTableViewStyleChanged:(UISegmentedControl *)segmentedControl { + self.tableView = [[QMUITableView alloc] initWithFrame:self.view.bounds style:segmentedControl.selectedSegmentIndex]; + self.tableView.dataSource = self; + self.tableView.delegate = self; + [self.view sendSubviewToBack:self.tableView]; +} + +@end diff --git a/qmuidemo/Modules/Common/LookinConfig/LookinConfig.h b/qmuidemo/Modules/Common/LookinConfig/LookinConfig.h new file mode 100644 index 00000000..a9b6f14e --- /dev/null +++ b/qmuidemo/Modules/Common/LookinConfig/LookinConfig.h @@ -0,0 +1,17 @@ +// +// LookinConfig.h +// Lookin +// +// Copyright © 2019 Lookin. All rights reserved. + +#import + +/** + The config of Lookin app. Visit the website below for more tutorials. + Lookin 的个性化配置文件,点击下方链接可了解更多。 + + https://lookin.work/faq/config-file/ + */ +@interface LookinConfig : NSObject + +@end diff --git a/qmuidemo/Modules/Common/LookinConfig/LookinConfig.m b/qmuidemo/Modules/Common/LookinConfig/LookinConfig.m new file mode 100644 index 00000000..b787d541 --- /dev/null +++ b/qmuidemo/Modules/Common/LookinConfig/LookinConfig.m @@ -0,0 +1,51 @@ +// +// LookinConfig.m +// Lookin +// +// Copyright © 2019 Lookin. All rights reserved. + +#import "LookinConfig.h" + +@implementation LookinConfig + +/** + Enable Lookin app to display colors with custom names. + Available since Lookin v0.9.2 (Released at 2019-07-23). + + 让 Lookin 显示 UIColor 在您业务里的自定义名称,而非仅仅展示一个色值。 + 该配置从 2019-07-23 发布的 Lookin 0.9.2 版本开始生效。 + + https://lookin.work/faq/config-file/#colors + */ ++ (NSDictionary *)colors { + return @{ + @"qd_backgroundColor": UIColor.qd_backgroundColor, + @"qd_backgroundColorLighten": UIColor.qd_backgroundColorLighten, + @"qd_backgroundColorHighlighted": UIColor.qd_backgroundColorHighlighted, + @"qd_tintColor": UIColor.qd_tintColor, + @"qd_titleTextColor": UIColor.qd_titleTextColor, + @"qd_mainTextColor": UIColor.qd_mainTextColor, + @"qd_descriptionTextColor": UIColor.qd_descriptionTextColor, + @"qd_placeholderColor": UIColor.qd_placeholderColor, + @"qd_codeColor": UIColor.qd_codeColor, + @"qd_separatorColor": UIColor.qd_separatorColor, + @"qd_gridItemTintColor": UIColor.qd_gridItemTintColor, + }; +} + +/** + There are some kind of views that you rarely want to expand its hierarchy to inspect its subviews, e.g. UISlider, UIButton. Return the class names in the method below and Lookin will collapse them in most situations to keep your workspace uncluttered. + Available since Lookin v0.9.2 (Released at 2019-07-23). + + 有一些类我们很少有需求去查看它的 subviews 结构,比如 UISlider, UIButton。把这些不常展开的类的类名在下面的方法里返回,Lookin 将尽可能折叠这些类的图像,从而让你的工作区更加整洁。 + 该配置从 2019-07-23 发布的 Lookin 0.9.2 版本开始生效。 + + https://lookin.work/faq/config-file/#collapsed-classes + */ ++ (NSArray *)collapsedClasses { +// example: +// return @[@"AvatarButton", @"BookCoverView"]; + return nil; +} + +@end diff --git a/qmuidemo/Modules/Common/Utils/QDCommonUI.h b/qmuidemo/Modules/Common/Utils/QDCommonUI.h index 073d1db4..37458b41 100644 --- a/qmuidemo/Modules/Common/Utils/QDCommonUI.h +++ b/qmuidemo/Modules/Common/Utils/QDCommonUI.h @@ -20,20 +20,38 @@ #define UIColorGray8 UIColorMake(196, 200, 208) #define UIColorGray9 UIColorMake(216, 220, 228) +#define UIColorDarkGray1 UIColorMake(218, 220, 224) +#define UIColorDarkGray2 UIColorMake(198, 200, 204) +#define UIColorDarkGray3 UIColorMake(178, 180, 184) +#define UIColorDarkGray4 UIColorMake(158, 160, 164) +#define UIColorDarkGray5 UIColorMake(138, 140, 144) +#define UIColorDarkGray6 UIColorMake(118, 120, 124) +#define UIColorDarkGray7 UIColorMake(98, 100, 104) +#define UIColorDarkGray8 UIColorMake(78, 80, 84) +#define UIColorDarkGray9 UIColorMake(58, 60, 64) + #define UIColorTheme1 UIColorMake(239, 83, 98) // Grapefruit #define UIColorTheme2 UIColorMake(254, 109, 75) // Bittersweet #define UIColorTheme3 UIColorMake(255, 207, 71) // Sunflower #define UIColorTheme4 UIColorMake(159, 214, 97) // Grass #define UIColorTheme5 UIColorMake(63, 208, 173) // Mint -#define UIColorTheme6 UIColorMake(49, 189, 243) // Aqua +#define UIColorTheme6 UIColorMake(6, 92, 208) // Klein #define UIColorTheme7 UIColorMake(90, 154, 239) // Blue Jeans #define UIColorTheme8 UIColorMake(172, 143, 239) // Lavender #define UIColorTheme9 UIColorMake(238, 133, 193) // Pink Rose +#define UIColorTheme10 UIColorMake(39, 192, 243) // Dark -extern NSString *const QDSelectedThemeClassName; +extern NSString *const QDSelectedThemeIdentifier; +extern NSString *const QDThemeIdentifierDefault; +extern NSString *const QDThemeIdentifierGrapefruit; +extern NSString *const QDThemeIdentifierGrass; +extern NSString *const QDThemeIdentifierPinkRose; +extern NSString *const QDThemeIdentifierDark; #define CodeFontMake(_pointSize) [UIFont fontWithName:@"Menlo" size:_pointSize] -#define CodeAttributes(_fontSize) @{NSFontAttributeName: CodeFontMake(_fontSize), NSForegroundColorAttributeName: [QDThemeManager sharedInstance].currentTheme.themeCodeColor} +#define CodeAttributes(_fontSize) @{NSFontAttributeName: CodeFontMake(_fontSize), NSForegroundColorAttributeName: QDThemeManager.currentTheme.themeCodeColor} + +#define IsUITest NSProcessInfo.processInfo.environment[@"isUITest"].boolValue /// QMUIButton 系列 Demo 里的一行高度 extern const CGFloat QDButtonSpacingHeight; diff --git a/qmuidemo/Modules/Common/Utils/QDCommonUI.m b/qmuidemo/Modules/Common/Utils/QDCommonUI.m index 1b5284a1..98468e53 100644 --- a/qmuidemo/Modules/Common/Utils/QDCommonUI.m +++ b/qmuidemo/Modules/Common/Utils/QDCommonUI.m @@ -9,21 +9,63 @@ #import "QDCommonUI.h" #import "QDUIHelper.h" -NSString *const QDSelectedThemeClassName = @"selectedThemeClassName"; +NSString *const QDSelectedThemeIdentifier = @"selectedThemeIdentifier"; +NSString *const QDThemeIdentifierDefault = @"Default"; +NSString *const QDThemeIdentifierGrapefruit = @"Grapefruit"; +NSString *const QDThemeIdentifierGrass = @"Grass"; +NSString *const QDThemeIdentifierPinkRose = @"Pink Rose"; +NSString *const QDThemeIdentifierDark = @"Dark"; const CGFloat QDButtonSpacingHeight = 72; @implementation QDCommonUI ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // 统一设置所有 QMUISearchController 搜索状态下的 statusBarStyle + OverrideImplementation([QMUISearchController class], @selector(initWithContentsViewController:resultsViewController:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^QMUISearchController *(QMUISearchController *selfObject, UIViewController *firstArgv, UIViewController *secondArgv) { + + // call super + QMUISearchController *(*originSelectorIMP)(id, SEL, UIViewController *, UIViewController *); + originSelectorIMP = (QMUISearchController * (*)(id, SEL, UIViewController *, UIViewController *))originalIMPProvider(); + QMUISearchController *result = originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); + + result.qmui_preferredStatusBarStyleBlock = ^UIStatusBarStyle{ + if ([QMUIThemeManagerCenter.defaultThemeManager.currentThemeIdentifier isEqual:QDThemeIdentifierDark]) { + return UIStatusBarStyleLightContent; + } + return UIStatusBarStyleDarkContent; + }; + return result; + }; + }); + }); +} + + (void)renderGlobalAppearances { [QDUIHelper customMoreOperationAppearance]; [QDUIHelper customAlertControllerAppearance]; [QDUIHelper customDialogViewControllerAppearance]; [QDUIHelper customImagePickerAppearance]; [QDUIHelper customEmotionViewAppearance]; + [QDUIHelper customPopupAppearance]; UISearchBar *searchBar = [UISearchBar appearance]; searchBar.searchTextPositionAdjustment = UIOffsetMake(4, 0); + searchBar.qmui_centerPlaceholder = YES; + + QMUILabel *label = [QMUILabel appearance]; + label.highlightedBackgroundColor = TableViewCellSelectedBackgroundColor; + + QMUINavigationTitleView *titleView = QMUINavigationTitleView.appearance; + titleView.verticalTitleFont = NavBarTitleFont; + + UISlider *slider = UISlider.appearance; + slider.minimumTrackTintColor = UIColor.qd_tintColor; + slider.maximumTrackTintColor = UIColor.qd_separatorColor; + slider.qmui_thumbColor = UIColor.qd_tintColor; } @end @@ -41,9 +83,10 @@ + (UIColor *)randomThemeColor { UIColorTheme6, UIColorTheme7, UIColorTheme8, - UIColorTheme9]; + UIColorTheme9, + UIColorTheme10]; } - return themeColors[arc4random() % 9]; + return themeColors[arc4random() % themeColors.count]; } @end diff --git a/qmuidemo/Modules/Common/Utils/QDUIHelper.h b/qmuidemo/Modules/Common/Utils/QDUIHelper.h index 9f5bc8f6..ab88a7bc 100644 --- a/qmuidemo/Modules/Common/Utils/QDUIHelper.h +++ b/qmuidemo/Modules/Common/Utils/QDUIHelper.h @@ -10,8 +10,6 @@ @interface QDUIHelper : NSObject -+ (void)forceInterfaceOrientationPortrait; - @end @@ -47,6 +45,11 @@ @end +@interface QDUIHelper (QMUIPopupContainerView) + ++ (void)customPopupAppearance; +@end + @interface QDUIHelper (UITabBarItem) @@ -59,7 +62,7 @@ + (QMUIButton *)generateDarkFilledButton; + (QMUIButton *)generateLightBorderedButton; - ++ (QMUIButton *)generateGhostButtonWithColor:(UIColor *)color; @end @@ -91,6 +94,14 @@ + (UIImage *)navigationBarBackgroundImageWithThemeColor:(UIColor *)color; @end +@class QMUIInteractiveDebugPanelViewController; +@class QMUIInteractiveDebugPanelItem; + +@interface QDUIHelper (Debug) + ++ (QMUIInteractiveDebugPanelViewController *)generateDebugViewControllerWithTitle:(NSString *)title items:(NSArray *)items; +@end + @interface NSString (Code) diff --git a/qmuidemo/Modules/Common/Utils/QDUIHelper.m b/qmuidemo/Modules/Common/Utils/QDUIHelper.m index dfd19290..212f7cc9 100644 --- a/qmuidemo/Modules/Common/Utils/QDUIHelper.m +++ b/qmuidemo/Modules/Common/Utils/QDUIHelper.m @@ -7,11 +7,30 @@ // #import "QDUIHelper.h" +#import "QMUIInteractiveDebugger.h" @implementation QDUIHelper -+ (void)forceInterfaceOrientationPortrait { - [QMUIHelper rotateToDeviceOrientation:UIDeviceOrientationPortrait]; ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ExtendImplementationOfNonVoidMethodWithSingleArgument([UIControl class], @selector(initWithFrame:), CGRect, UIControl *, ^UIControl *(UIControl *selfObject, CGRect firstArgv, UIControl * originReturnValue) { + ({ + NSArray *controlClasses = @[ + NSClassFromString(@"_UIButtonBarButton"),// iOS 11 及以后的 UIBarButtonItem 按钮 + QMUIButton.class, + QMUINavigationButton.class + ]; + for (Class className in controlClasses) { + if ([selfObject isKindOfClass:className]) { + originReturnValue.qmui_preventsRepeatedTouchUpInsideEvent = YES; + break; + } + } + }); + return originReturnValue; + }); + }); } @end @@ -21,7 +40,7 @@ @implementation QDUIHelper (QMUIMoreOperationAppearance) + (void)customMoreOperationAppearance { // 如果需要统一修改全局的 QMUIMoreOperationController 样式,在这里修改 appearance 的值即可 - [QMUIMoreOperationController appearance].cancelButtonTitleColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + [QMUIMoreOperationController appearance].cancelButtonTitleColor = UIColor.qd_tintColor; } @end @@ -40,12 +59,32 @@ @implementation QDUIHelper (QMUIDialogViewControllerAppearance) + (void)customDialogViewControllerAppearance { // 如果需要统一修改全局的 QMUIDialogViewController 样式,在这里修改 appearance 的值即可 QMUIDialogViewController *appearance = [QMUIDialogViewController appearance]; + appearance.backgroundColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject * _Nullable theme) { + if ([identifier isEqualToString:QDThemeIdentifierDark]) { + return UIColorMake(34, 34, 34); + } + return UIColorWhite; + }]; + appearance.headerViewBackgroundColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject * _Nullable theme) { + if ([identifier isEqualToString:QDThemeIdentifierDark]) { + return UIColorMake(34, 34, 34); + } + return UIColorMake(244, 245, 247); + }]; + appearance.contentViewBackgroundColor = appearance.backgroundColor; + appearance.headerSeparatorColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject * _Nullable theme) { + if ([identifier isEqualToString:QDThemeIdentifierDark]) { + return UIColorMake(51, 51, 51); + } + return UIColorMake(222, 224, 226); + }]; + appearance.footerSeparatorColor = appearance.headerSeparatorColor; NSMutableDictionary *buttonTitleAttributes = [appearance.buttonTitleAttributes mutableCopy]; - buttonTitleAttributes[NSForegroundColorAttributeName] = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + buttonTitleAttributes[NSForegroundColorAttributeName] = UIColor.qd_tintColor; appearance.buttonTitleAttributes = [buttonTitleAttributes copy]; - appearance.buttonHighlightedBackgroundColor = [[QDThemeManager sharedInstance].currentTheme.themeTintColor colorWithAlphaComponent:.25]; + appearance.buttonHighlightedBackgroundColor = [UIColor.qd_tintColor colorWithAlphaComponent:.25]; } @end @@ -56,7 +95,7 @@ @implementation QDUIHelper (QMUIEmotionView) + (void)customEmotionViewAppearance { [QMUIEmotionView appearance].emotionSize = CGSizeMake(24, 24); [QMUIEmotionView appearance].minimumEmotionHorizontalSpacing = 14; - [QMUIEmotionView appearance].sendButtonBackgroundColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + [QMUIEmotionView appearance].sendButtonBackgroundColor = UIColor.qd_tintColor; } @end @@ -66,9 +105,26 @@ @implementation QDUIHelper (QMUIImagePicker) + (void)customImagePickerAppearance { UIImage *checkboxImage = [QMUIHelper imageWithName:@"QMUI_pickerImage_checkbox"]; UIImage *checkboxCheckedImage = [QMUIHelper imageWithName:@"QMUI_pickerImage_checkbox_checked"]; - [QMUIImagePickerCollectionViewCell appearance].checkboxImage = [checkboxImage qmui_imageWithTintColor:[QDThemeManager sharedInstance].currentTheme.themeTintColor]; - [QMUIImagePickerCollectionViewCell appearance].checkboxCheckedImage = [checkboxCheckedImage qmui_imageWithTintColor:[QDThemeManager sharedInstance].currentTheme.themeTintColor]; - [QMUIImagePickerPreviewViewController appearance].toolBarTintColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + [QMUIImagePickerCollectionViewCell appearance].checkboxImage = [checkboxImage qmui_imageWithTintColor:UIColor.qd_tintColor]; + [QMUIImagePickerCollectionViewCell appearance].checkboxCheckedImage = [checkboxCheckedImage qmui_imageWithTintColor:UIColor.qd_tintColor]; + [QMUIImagePickerPreviewViewController appearance].toolBarTintColor = UIColor.qd_tintColor; +} + +@end + +@implementation QDUIHelper (QMUIPopupContainerView) + ++ (void)customPopupAppearance { + QMUIPopupContainerView *popup = QMUIPopupContainerView.appearance; + popup.backgroundColor = UIColor.qd_backgroundColor; + popup.borderColor = UIColor.qd_separatorColor; + popup.maskViewBackgroundColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return [identifier isEqual:QDThemeIdentifierDark] ? UIColorMask : UIColorMaskWhite; + }]; + + QMUIPopupMenuView *menuView = QMUIPopupMenuView.appearance; + menuView.itemSeparatorColor = UIColor.qd_separatorColor; + menuView.sectionSeparatorColor = UIColor.qd_separatorColor; } @end @@ -90,23 +146,46 @@ + (QMUIButton *)generateDarkFilledButton { QMUIButton *button = [[QMUIButton alloc] qmui_initWithSize:CGSizeMake(200, 40)]; button.adjustsButtonWhenHighlighted = YES; button.titleLabel.font = UIFontBoldMake(14); + button.subtitleLabel.font = UIFontMake(12); [button setTitleColor:UIColorWhite forState:UIControlStateNormal]; - button.backgroundColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; - button.highlightedBackgroundColor = [[QDThemeManager sharedInstance].currentTheme.themeTintColor qmui_transitionToColor:UIColorBlack progress:.15];// 高亮时的背景色 + button.subtitleColor = UIColorWhite; + button.backgroundColor = UIColor.qd_tintColor; + button.highlightedBackgroundColor = [UIColor.qd_tintColor qmui_transitionToColor:UIColorBlack progress:.15];// 高亮时的背景色 button.layer.cornerRadius = 4; return button; } + (QMUIButton *)generateLightBorderedButton { QMUIButton *button = [[QMUIButton alloc] qmui_initWithSize:CGSizeMake(200, 40)]; + __weak __typeof(button)weakButton = button; button.titleLabel.font = UIFontBoldMake(14); - [button setTitleColor:[QDThemeManager sharedInstance].currentTheme.themeTintColor forState:UIControlStateNormal]; - button.backgroundColor = [[QDThemeManager sharedInstance].currentTheme.themeTintColor qmui_transitionToColor:UIColorWhite progress:.9]; - button.highlightedBackgroundColor = [[QDThemeManager sharedInstance].currentTheme.themeTintColor qmui_transitionToColor:UIColorWhite progress:.75];// 高亮时的背景色 - button.layer.borderColor = [button.backgroundColor qmui_transitionToColor:[QDThemeManager sharedInstance].currentTheme.themeTintColor progress:.5].CGColor; + button.tintColorAdjustsTitleAndImage = UIColor.qd_tintColor; + button.backgroundColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return [UIColor.qd_tintColor qmui_transitionToColor:UIColor.qd_backgroundColor progress:.9]; + }]; + button.highlightedBackgroundColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return [UIColor.qd_tintColor qmui_transitionToColor:UIColor.qd_backgroundColor progress:.75]; + }];// 高亮时的背景色 + button.layer.borderColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return [weakButton.backgroundColor qmui_transitionToColor:UIColor.qd_tintColor progress:.5]; + }].CGColor; button.layer.borderWidth = 1; button.layer.cornerRadius = 4; - button.highlightedBorderColor = [button.backgroundColor qmui_transitionToColor:[QDThemeManager sharedInstance].currentTheme.themeTintColor progress:.9];// 高亮时的边框颜色 + button.highlightedBorderColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return [weakButton.backgroundColor qmui_transitionToColor:UIColor.qd_tintColor progress:.9]; + }];// 高亮时的边框颜色 + button.qmui_setSelectedBlock = ^(BOOL selected) { + weakButton.layer.borderWidth = selected ? 2 : 1; + }; + return button; +} + ++ (QMUIButton *)generateGhostButtonWithColor:(UIColor *)color { + QMUIButton *button = [[QMUIButton alloc] init]; + [button setTitleColor:color forState:UIControlStateNormal]; + button.layer.borderColor = color.CGColor; + button.layer.borderWidth = 1; + button.cornerRadius = QMUIButtonCornerRadiusAdjustsBounds; return button; } @@ -115,7 +194,21 @@ + (QMUIButton *)generateLightBorderedButton { @implementation QDUIHelper (Emotion) -NSString *const QMUIEmotionString = @"01-[微笑];02-[开心];03-[生气];04-[委屈];05-[亲亲];06-[坏笑];07-[鄙视];08-[啊]"; +NSString *const QMUIEmotionString = +@"01-[微笑];02-[开心];03-[生气];04-[委屈];05-[亲亲];06-[坏笑];07-[鄙视];08-[啊]\ +01-[微笑];02-[开心];03-[生气];04-[委屈];05-[亲亲];06-[坏笑];07-[鄙视];08-[啊]\ +01-[微笑];02-[开心];03-[生气];04-[委屈];05-[亲亲];06-[坏笑];07-[鄙视];08-[啊]\ +01-[微笑];02-[开心];03-[生气];04-[委屈];05-[亲亲];06-[坏笑];07-[鄙视];08-[啊]\ +01-[微笑];02-[开心];03-[生气];04-[委屈];05-[亲亲];06-[坏笑];07-[鄙视];08-[啊]\ +01-[微笑];02-[开心];03-[生气];04-[委屈];05-[亲亲];06-[坏笑];07-[鄙视];08-[啊]\ +01-[微笑];02-[开心];03-[生气];04-[委屈];05-[亲亲];06-[坏笑];07-[鄙视];08-[啊]\ +01-[微笑];02-[开心];03-[生气];04-[委屈];05-[亲亲];06-[坏笑];07-[鄙视];08-[啊]\ +01-[微笑];02-[开心];03-[生气];04-[委屈];05-[亲亲];06-[坏笑];07-[鄙视];08-[啊]\ +01-[微笑];02-[开心];03-[生气];04-[委屈];05-[亲亲];06-[坏笑];07-[鄙视];08-[啊]\ +01-[微笑];02-[开心];03-[生气];04-[委屈];05-[亲亲];06-[坏笑];07-[鄙视];08-[啊]\ +01-[微笑];02-[开心];03-[生气];04-[委屈];05-[亲亲];06-[坏笑];07-[鄙视];08-[啊]\ +01-[微笑];02-[开心];03-[生气];04-[委屈];05-[亲亲];06-[坏笑];07-[鄙视];08-[啊]\ +"; static NSArray *QMUIEmotionArray; @@ -140,9 +233,9 @@ @implementation QDUIHelper (Emotion) // 在子线程预加载 + (void)asyncLoadImages:(NSArray *)emotions { - dispatch_async(dispatch_get_global_queue(0, 0), ^{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ for (QMUIEmotion *e in emotions) { - e.image = [UIImageMake(e.identifier) qmui_imageWithBlendColor:[QDThemeManager sharedInstance].currentTheme.themeTintColor]; + e.image = [UIImageMake(e.identifier) qmui_imageWithBlendColor:UIColor.qd_tintColor]; } }); } @@ -211,6 +304,34 @@ + (UIImage *)navigationBarBackgroundImageWithThemeColor:(UIColor *)color { @end +@implementation QDUIHelper (Debug) + ++ (QMUIInteractiveDebugPanelViewController *)generateDebugViewControllerWithTitle:(NSString *)title items:(NSArray *)items { + QMUIInteractiveDebugPanelViewController *vc = [[QMUIInteractiveDebugPanelViewController alloc] init]; + vc.title = title; + [items enumerateObjectsUsingBlock:^(QMUIInteractiveDebugPanelItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [vc addDebugItem:obj]; + }]; + vc.styleConfiguration = ^(QMUIInteractiveDebugPanelViewController * _Nonnull viewController) { + viewController.view.backgroundColor = UIColor.qd_backgroundColorLighten; + viewController.view.layer.borderColor = UIColor.qd_separatorColor.CGColor; + viewController.titleLabel.textColor = UIColor.qd_titleTextColor; + [viewController.debugItems enumerateObjectsUsingBlock:^(QMUIInteractiveDebugPanelItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + obj.titleLabel.textColor = UIColor.qd_titleTextColor; + obj.actionView.tintColor = UIColor.qd_tintColor; + if ([obj.actionView isKindOfClass:UITextField.class]) { + ((UITextField *)obj.actionView).textColor = UIColor.qd_titleTextColor; + ((UITextField *)obj.actionView).qmui_borderColor = UIColor.qd_separatorColor; + } else if ([obj.actionView isKindOfClass:QMUIButton.class]) { + ((QMUIButton *)obj.actionView).backgroundColor = [UIColor.qd_backgroundColorHighlighted colorWithAlphaComponent:.5]; + } + }]; + }; + return vc; +} + +@end + @implementation NSString (Code) diff --git a/qmuidemo/Modules/Debug/Aspects/Aspects.h b/qmuidemo/Modules/Debug/Aspects/Aspects.h new file mode 100644 index 00000000..5508f862 --- /dev/null +++ b/qmuidemo/Modules/Debug/Aspects/Aspects.h @@ -0,0 +1,83 @@ +// +// Aspects.h +// Aspects - A delightful, simple library for aspect oriented programming. +// +// Copyright (c) 2014 Peter Steinberger. Licensed under the MIT license. +// + +#import + +typedef NS_OPTIONS(NSUInteger, AspectOptions) { + AspectPositionAfter = 0, /// Called after the original implementation (default) + AspectPositionInstead = 1, /// Will replace the original implementation. + AspectPositionBefore = 2, /// Called before the original implementation. + + AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution. +}; + +/// Opaque Aspect Token that allows to deregister the hook. +@protocol AspectToken + +/// Deregisters an aspect. +/// @return YES if deregistration is successful, otherwise NO. +- (BOOL)remove; + +@end + +/// The AspectInfo protocol is the first parameter of our block syntax. +@protocol AspectInfo + +/// The instance that is currently hooked. +- (id)instance; + +/// The original invocation of the hooked method. +- (NSInvocation *)originalInvocation; + +/// All method arguments, boxed. This is lazily evaluated. +- (NSArray *)arguments; + +@end + +/** + Aspects uses Objective-C message forwarding to hook into messages. This will create some overhead. Don't add aspects to methods that are called a lot. Aspects is meant for view/controller code that is not called a 1000 times per second. + + Adding aspects returns an opaque token which can be used to deregister again. All calls are thread safe. + */ +@interface NSObject (Aspects) + +/// Adds a block of code before/instead/after the current `selector` for a specific class. +/// +/// @param block Aspects replicates the type signature of the method being hooked. +/// The first parameter will be `id`, followed by all parameters of the method. +/// These parameters are optional and will be filled to match the block signature. +/// You can even use an empty block, or one that simple gets `id`. +/// +/// @note Hooking static methods is not supported. +/// @return A token which allows to later deregister the aspect. ++ (id)aspect_hookSelector:(SEL)selector + withOptions:(AspectOptions)options + usingBlock:(id)block + error:(NSError **)error; + +/// Adds a block of code before/instead/after the current `selector` for a specific instance. +- (id)aspect_hookSelector:(SEL)selector + withOptions:(AspectOptions)options + usingBlock:(id)block + error:(NSError **)error; + +@end + + +typedef NS_ENUM(NSUInteger, AspectErrorCode) { + AspectErrorSelectorBlacklisted, /// Selectors like release, retain, autorelease are blacklisted. + AspectErrorDoesNotRespondToSelector, /// Selector could not be found. + AspectErrorSelectorDeallocPosition, /// When hooking dealloc, only AspectPositionBefore is allowed. + AspectErrorSelectorAlreadyHookedInClassHierarchy, /// Statically hooking the same method in subclasses is not allowed. + AspectErrorFailedToAllocateClassPair, /// The runtime failed creating a class pair. + AspectErrorMissingBlockSignature, /// The block misses compile time signature info and can't be called. + AspectErrorIncompatibleBlockSignature, /// The block signature does not match the method or is too large. + + AspectErrorRemoveObjectAlreadyDeallocated = 100 /// (for removing) The object hooked is already deallocated. +}; + +extern NSString *const AspectErrorDomain; diff --git a/qmuidemo/Modules/Debug/Aspects/Aspects.m b/qmuidemo/Modules/Debug/Aspects/Aspects.m new file mode 100644 index 00000000..94195c47 --- /dev/null +++ b/qmuidemo/Modules/Debug/Aspects/Aspects.m @@ -0,0 +1,945 @@ +// +// Aspects.m +// Aspects - A delightful, simple library for aspect oriented programming. +// +// Copyright (c) 2014 Peter Steinberger. Licensed under the MIT license. +// + +#import "Aspects.h" +#import +#import +#import + +#define AspectLog(...) +//#define AspectLog(...) do { NSLog(__VA_ARGS__); }while(0) +#define AspectLogError(...) do { NSLog(__VA_ARGS__); }while(0) + +// Block internals. +typedef NS_OPTIONS(int, AspectBlockFlags) { + AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25), + AspectBlockFlagsHasSignature = (1 << 30) +}; +typedef struct _AspectBlock { + __unused Class isa; + AspectBlockFlags flags; + __unused int reserved; + void (__unused *invoke)(struct _AspectBlock *block, ...); + struct { + unsigned long int reserved; + unsigned long int size; + // requires AspectBlockFlagsHasCopyDisposeHelpers + void (*copy)(void *dst, const void *src); + void (*dispose)(const void *); + // requires AspectBlockFlagsHasSignature + const char *signature; + const char *layout; + } *descriptor; + // imported variables +} *AspectBlockRef; + +@interface AspectInfo : NSObject +- (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation; +@property (nonatomic, unsafe_unretained, readonly) id instance; +@property (nonatomic, strong, readonly) NSArray *arguments; +@property (nonatomic, strong, readonly) NSInvocation *originalInvocation; +@end + +// Tracks a single aspect. +@interface AspectIdentifier : NSObject ++ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error; +- (BOOL)invokeWithInfo:(id)info; +@property (nonatomic, assign) SEL selector; +@property (nonatomic, strong) id block; +@property (nonatomic, strong) NSMethodSignature *blockSignature; +@property (nonatomic, weak) id object; +@property (nonatomic, assign) AspectOptions options; +@end + +// Tracks all aspects for an object/class. +@interface AspectsContainer : NSObject +- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition; +- (BOOL)removeAspect:(id)aspect; +- (BOOL)hasAspects; +@property (atomic, copy) NSArray *beforeAspects; +@property (atomic, copy) NSArray *insteadAspects; +@property (atomic, copy) NSArray *afterAspects; +@end + +@interface AspectTracker : NSObject +- (id)initWithTrackedClass:(Class)trackedClass; +@property (nonatomic, strong) Class trackedClass; +@property (nonatomic, readonly) NSString *trackedClassName; +@property (nonatomic, strong) NSMutableSet *selectorNames; +@property (nonatomic, strong) NSMutableDictionary *selectorNamesToSubclassTrackers; +- (void)addSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName; +- (void)removeSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName; +- (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName; +- (NSSet *)subclassTrackersHookingSelectorName:(NSString *)selectorName; +@end + +@interface NSInvocation (Aspects) +- (NSArray *)aspects_arguments; +@end + +#define AspectPositionFilter 0x07 + +#define AspectError(errorCode, errorDescription) do { \ +AspectLogError(@"Aspects: %@", errorDescription); \ +if (error) { *error = [NSError errorWithDomain:AspectErrorDomain code:errorCode userInfo:@{NSLocalizedDescriptionKey: errorDescription}]; }}while(0) + +NSString *const AspectErrorDomain = @"AspectErrorDomain"; +static NSString *const AspectsSubclassSuffix = @"_Aspects_"; +static NSString *const AspectsMessagePrefix = @"aspects_"; + +@implementation NSObject (Aspects) + +/////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - Public Aspects API + ++ (id)aspect_hookSelector:(SEL)selector + withOptions:(AspectOptions)options + usingBlock:(id)block + error:(NSError **)error { + return aspect_add((id)self, selector, options, block, error); +} + +/// @return A token which allows to later deregister the aspect. +- (id)aspect_hookSelector:(SEL)selector + withOptions:(AspectOptions)options + usingBlock:(id)block + error:(NSError **)error { + return aspect_add(self, selector, options, block, error); +} + +/////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - Private Helper + +static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) { + NSCParameterAssert(self); + NSCParameterAssert(selector); + NSCParameterAssert(block); + + __block AspectIdentifier *identifier = nil; + aspect_performLocked(^{ + if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) { + AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector); + identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error]; + if (identifier) { + [aspectContainer addAspect:identifier withOptions:options]; + + // Modify the class to allow message interception. + aspect_prepareClassAndHookSelector(self, selector, error); + } + } + }); + return identifier; +} + +static BOOL aspect_remove(AspectIdentifier *aspect, NSError **error) { + NSCAssert([aspect isKindOfClass:AspectIdentifier.class], @"Must have correct type."); + + __block BOOL success = NO; + aspect_performLocked(^{ + id self = aspect.object; // strongify + if (self) { + AspectsContainer *aspectContainer = aspect_getContainerForObject(self, aspect.selector); + success = [aspectContainer removeAspect:aspect]; + + aspect_cleanupHookedClassAndSelector(self, aspect.selector); + // destroy token + aspect.object = nil; + aspect.block = nil; + aspect.selector = NULL; + }else { + NSString *errrorDesc = [NSString stringWithFormat:@"Unable to deregister hook. Object already deallocated: %@", aspect]; + AspectError(AspectErrorRemoveObjectAlreadyDeallocated, errrorDesc); + } + }); + return success; +} + +static void aspect_performLocked(dispatch_block_t block) { + static OSSpinLock aspect_lock = OS_SPINLOCK_INIT; + OSSpinLockLock(&aspect_lock); + block(); + OSSpinLockUnlock(&aspect_lock); +} + +static SEL aspect_aliasForSelector(SEL selector) { + NSCParameterAssert(selector); + return NSSelectorFromString([AspectsMessagePrefix stringByAppendingFormat:@"_%@", NSStringFromSelector(selector)]); +} + +static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) { + AspectBlockRef layout = (__bridge void *)block; + if (!(layout->flags & AspectBlockFlagsHasSignature)) { + NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block]; + AspectError(AspectErrorMissingBlockSignature, description); + return nil; + } + void *desc = layout->descriptor; + desc += 2 * sizeof(unsigned long int); + if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) { + desc += 2 * sizeof(void *); + } + if (!desc) { + NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block]; + AspectError(AspectErrorMissingBlockSignature, description); + return nil; + } + const char *signature = (*(const char **)desc); + return [NSMethodSignature signatureWithObjCTypes:signature]; +} + +static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) { + NSCParameterAssert(blockSignature); + NSCParameterAssert(object); + NSCParameterAssert(selector); + + BOOL signaturesMatch = YES; + NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector]; + if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) { + signaturesMatch = NO; + }else { + if (blockSignature.numberOfArguments > 1) { + const char *blockType = [blockSignature getArgumentTypeAtIndex:1]; + if (blockType[0] != '@') { + signaturesMatch = NO; + } + } + // Argument 0 is self/block, argument 1 is SEL or id. We start comparing at argument 2. + // The block can have less arguments than the method, that's ok. + if (signaturesMatch) { + for (NSUInteger idx = 2; idx < blockSignature.numberOfArguments; idx++) { + const char *methodType = [methodSignature getArgumentTypeAtIndex:idx]; + const char *blockType = [blockSignature getArgumentTypeAtIndex:idx]; + // Only compare parameter, not the optional type data. + if (!methodType || !blockType || methodType[0] != blockType[0]) { + signaturesMatch = NO; break; + } + } + } + } + + if (!signaturesMatch) { + NSString *description = [NSString stringWithFormat:@"Block signature %@ doesn't match %@.", blockSignature, methodSignature]; + AspectError(AspectErrorIncompatibleBlockSignature, description); + return NO; + } + return YES; +} + +/////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - Class + Selector Preparation + +static BOOL aspect_isMsgForwardIMP(IMP impl) { + return impl == _objc_msgForward +#if !defined(__arm64__) + || impl == (IMP)_objc_msgForward_stret +#endif + ; +} + +static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) { + IMP msgForwardIMP = _objc_msgForward; +#if !defined(__arm64__) + // As an ugly internal runtime implementation detail in the 32bit runtime, we need to determine of the method we hook returns a struct or anything larger than id. + // https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html + // https://github.com/ReactiveCocoa/ReactiveCocoa/issues/783 + // http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf (Section 5.4) + Method method = class_getInstanceMethod(self.class, selector); + const char *encoding = method_getTypeEncoding(method); + BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B; + if (methodReturnsStructValue) { + @try { + NSUInteger valueSize = 0; + NSGetSizeAndAlignment(encoding, &valueSize, NULL); + + if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) { + methodReturnsStructValue = NO; + } + } @catch (__unused NSException *e) {} + } + if (methodReturnsStructValue) { + msgForwardIMP = (IMP)_objc_msgForward_stret; + } +#endif + return msgForwardIMP; +} + +static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) { + NSCParameterAssert(selector); + Class klass = aspect_hookClass(self, error); + Method targetMethod = class_getInstanceMethod(klass, selector); + IMP targetMethodIMP = method_getImplementation(targetMethod); + if (!aspect_isMsgForwardIMP(targetMethodIMP)) { + // Make a method alias for the existing method implementation, it not already copied. + const char *typeEncoding = method_getTypeEncoding(targetMethod); + SEL aliasSelector = aspect_aliasForSelector(selector); + if (![klass instancesRespondToSelector:aliasSelector]) { + __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding); + NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass); + } + + // We use forwardInvocation to hook in. + class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding); + AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector)); + } +} + +// Will undo the runtime changes made. +static void aspect_cleanupHookedClassAndSelector(NSObject *self, SEL selector) { + NSCParameterAssert(self); + NSCParameterAssert(selector); + + Class klass = object_getClass(self); + BOOL isMetaClass = class_isMetaClass(klass); + if (isMetaClass) { + klass = (Class)self; + } + + // Check if the method is marked as forwarded and undo that. + Method targetMethod = class_getInstanceMethod(klass, selector); + IMP targetMethodIMP = method_getImplementation(targetMethod); + if (aspect_isMsgForwardIMP(targetMethodIMP)) { + // Restore the original method implementation. + const char *typeEncoding = method_getTypeEncoding(targetMethod); + SEL aliasSelector = aspect_aliasForSelector(selector); + Method originalMethod = class_getInstanceMethod(klass, aliasSelector); + IMP originalIMP = method_getImplementation(originalMethod); + NSCAssert(originalMethod, @"Original implementation for %@ not found %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass); + + class_replaceMethod(klass, selector, originalIMP, typeEncoding); + AspectLog(@"Aspects: Removed hook for -[%@ %@].", klass, NSStringFromSelector(selector)); + } + + // Deregister global tracked selector + aspect_deregisterTrackedSelector(self, selector); + + // Get the aspect container and check if there are any hooks remaining. Clean up if there are not. + AspectsContainer *container = aspect_getContainerForObject(self, selector); + if (!container.hasAspects) { + // Destroy the container + aspect_destroyContainerForObject(self, selector); + + // Figure out how the class was modified to undo the changes. + NSString *className = NSStringFromClass(klass); + if ([className hasSuffix:AspectsSubclassSuffix]) { + Class originalClass = NSClassFromString([className stringByReplacingOccurrencesOfString:AspectsSubclassSuffix withString:@""]); + NSCAssert(originalClass != nil, @"Original class must exist"); + object_setClass(self, originalClass); + AspectLog(@"Aspects: %@ has been restored.", NSStringFromClass(originalClass)); + + // We can only dispose the class pair if we can ensure that no instances exist using our subclass. + // Since we don't globally track this, we can't ensure this - but there's also not much overhead in keeping it around. + //objc_disposeClassPair(object.class); + }else { + // Class is most likely swizzled in place. Undo that. + if (isMetaClass) { + aspect_undoSwizzleClassInPlace((Class)self); + }else if (self.class != klass) { + aspect_undoSwizzleClassInPlace(klass); + } + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - Hook Class + +static Class aspect_hookClass(NSObject *self, NSError **error) { + NSCParameterAssert(self); + Class statedClass = self.class; + Class baseClass = object_getClass(self); + NSString *className = NSStringFromClass(baseClass); + + // Already subclassed + if ([className hasSuffix:AspectsSubclassSuffix]) { + return baseClass; + + // We swizzle a class object, not a single object. + }else if (class_isMetaClass(baseClass)) { + return aspect_swizzleClassInPlace((Class)self); + // Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place. + }else if (statedClass != baseClass) { + return aspect_swizzleClassInPlace(baseClass); + } + + // Default case. Create dynamic subclass. + const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String; + Class subclass = objc_getClass(subclassName); + + if (subclass == nil) { + subclass = objc_allocateClassPair(baseClass, subclassName, 0); + if (subclass == nil) { + NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName]; + AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc); + return nil; + } + + aspect_swizzleForwardInvocation(subclass); + aspect_hookedGetClass(subclass, statedClass); + aspect_hookedGetClass(object_getClass(subclass), statedClass); + objc_registerClassPair(subclass); + } + + object_setClass(self, subclass); + return subclass; +} + +static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:"; +static void aspect_swizzleForwardInvocation(Class klass) { + NSCParameterAssert(klass); + // If there is no method, replace will act like class_addMethod. + IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@"); + if (originalImplementation) { + class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@"); + } + AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass)); +} + +static void aspect_undoSwizzleForwardInvocation(Class klass) { + NSCParameterAssert(klass); + Method originalMethod = class_getInstanceMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName)); + Method objectMethod = class_getInstanceMethod(NSObject.class, @selector(forwardInvocation:)); + // There is no class_removeMethod, so the best we can do is to retore the original implementation, or use a dummy. + IMP originalImplementation = method_getImplementation(originalMethod ?: objectMethod); + class_replaceMethod(klass, @selector(forwardInvocation:), originalImplementation, "v@:@"); + + AspectLog(@"Aspects: %@ has been restored.", NSStringFromClass(klass)); +} + +static void aspect_hookedGetClass(Class class, Class statedClass) { + NSCParameterAssert(class); + NSCParameterAssert(statedClass); + Method method = class_getInstanceMethod(class, @selector(class)); + IMP newIMP = imp_implementationWithBlock(^(id self) { + return statedClass; + }); + class_replaceMethod(class, @selector(class), newIMP, method_getTypeEncoding(method)); +} + +/////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - Swizzle Class In Place + +static void _aspect_modifySwizzledClasses(void (^block)(NSMutableSet *swizzledClasses)) { + static NSMutableSet *swizzledClasses; + static dispatch_once_t pred; + dispatch_once(&pred, ^{ + swizzledClasses = [NSMutableSet new]; + }); + @synchronized(swizzledClasses) { + block(swizzledClasses); + } +} + +static Class aspect_swizzleClassInPlace(Class klass) { + NSCParameterAssert(klass); + NSString *className = NSStringFromClass(klass); + + _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) { + if (![swizzledClasses containsObject:className]) { + aspect_swizzleForwardInvocation(klass); + [swizzledClasses addObject:className]; + } + }); + return klass; +} + +static void aspect_undoSwizzleClassInPlace(Class klass) { + NSCParameterAssert(klass); + NSString *className = NSStringFromClass(klass); + + _aspect_modifySwizzledClasses(^(NSMutableSet *swizzledClasses) { + if ([swizzledClasses containsObject:className]) { + aspect_undoSwizzleForwardInvocation(klass); + [swizzledClasses removeObject:className]; + } + }); +} + +/////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - Aspect Invoke Point + +// This is a macro so we get a cleaner stack trace. +#define aspect_invoke(aspects, info) \ +for (AspectIdentifier *aspect in aspects) {\ + [aspect invokeWithInfo:info];\ + if (aspect.options & AspectOptionAutomaticRemoval) { \ + aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \ + } \ +} + +// This is the swizzled forwardInvocation: method. +static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) { + NSCParameterAssert(self); + NSCParameterAssert(invocation); + SEL originalSelector = invocation.selector; + SEL aliasSelector = aspect_aliasForSelector(invocation.selector); + invocation.selector = aliasSelector; + AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector); + AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector); + AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation]; + NSArray *aspectsToRemove = nil; + + // Before hooks. + aspect_invoke(classContainer.beforeAspects, info); + aspect_invoke(objectContainer.beforeAspects, info); + + // Instead hooks. + BOOL respondsToAlias = YES; + if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) { + aspect_invoke(classContainer.insteadAspects, info); + aspect_invoke(objectContainer.insteadAspects, info); + }else { + Class klass = object_getClass(invocation.target); + do { + if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) { + [invocation invoke]; + break; + } + }while (!respondsToAlias && (klass = class_getSuperclass(klass))); + } + + // After hooks. + aspect_invoke(classContainer.afterAspects, info); + aspect_invoke(objectContainer.afterAspects, info); + + // If no hooks are installed, call original implementation (usually to throw an exception) + if (!respondsToAlias) { + invocation.selector = originalSelector; + SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName); + if ([self respondsToSelector:originalForwardInvocationSEL]) { + ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation); + }else { + [self doesNotRecognizeSelector:invocation.selector]; + } + } + + // Remove any hooks that are queued for deregistration. + [aspectsToRemove makeObjectsPerformSelector:@selector(remove)]; +} +#undef aspect_invoke + +/////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - Aspect Container Management + +// Loads or creates the aspect container. +static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) { + NSCParameterAssert(self); + SEL aliasSelector = aspect_aliasForSelector(selector); + AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector); + if (!aspectContainer) { + aspectContainer = [AspectsContainer new]; + objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN); + } + return aspectContainer; +} + +static AspectsContainer *aspect_getContainerForClass(Class klass, SEL selector) { + NSCParameterAssert(klass); + AspectsContainer *classContainer = nil; + do { + classContainer = objc_getAssociatedObject(klass, selector); + if (classContainer.hasAspects) break; + }while ((klass = class_getSuperclass(klass))); + + return classContainer; +} + +static void aspect_destroyContainerForObject(id self, SEL selector) { + NSCParameterAssert(self); + SEL aliasSelector = aspect_aliasForSelector(selector); + objc_setAssociatedObject(self, aliasSelector, nil, OBJC_ASSOCIATION_RETAIN); +} + +/////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - Selector Blacklist Checking + +static NSMutableDictionary *aspect_getSwizzledClassesDict() { + static NSMutableDictionary *swizzledClassesDict; + static dispatch_once_t pred; + dispatch_once(&pred, ^{ + swizzledClassesDict = [NSMutableDictionary new]; + }); + return swizzledClassesDict; +} + +static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) { + static NSSet *disallowedSelectorList; + static dispatch_once_t pred; + dispatch_once(&pred, ^{ + disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil]; + }); + + // Check against the blacklist. + NSString *selectorName = NSStringFromSelector(selector); + if ([disallowedSelectorList containsObject:selectorName]) { + NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName]; + AspectError(AspectErrorSelectorBlacklisted, errorDescription); + return NO; + } + + // Additional checks. + AspectOptions position = options&AspectPositionFilter; + if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) { + NSString *errorDesc = @"AspectPositionBefore is the only valid position when hooking dealloc."; + AspectError(AspectErrorSelectorDeallocPosition, errorDesc); + return NO; + } + + if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) { + NSString *errorDesc = [NSString stringWithFormat:@"Unable to find selector -[%@ %@].", NSStringFromClass(self.class), selectorName]; + AspectError(AspectErrorDoesNotRespondToSelector, errorDesc); + return NO; + } + + // Search for the current class and the class hierarchy IF we are modifying a class object + if (class_isMetaClass(object_getClass(self))) { + Class klass = [self class]; + NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict(); + Class currentClass = [self class]; + + AspectTracker *tracker = swizzledClassesDict[currentClass]; + if ([tracker subclassHasHookedSelectorName:selectorName]) { + NSSet *subclassTracker = [tracker subclassTrackersHookingSelectorName:selectorName]; + NSSet *subclassNames = [subclassTracker valueForKey:@"trackedClassName"]; + NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked subclasses: %@. A method can only be hooked once per class hierarchy.", selectorName, subclassNames]; + AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription); + return NO; + } + + do { + tracker = swizzledClassesDict[currentClass]; + if ([tracker.selectorNames containsObject:selectorName]) { + if (klass == currentClass) { + // Already modified and topmost! + return YES; + } + NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(currentClass)]; + AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription); + return NO; + } + } while ((currentClass = class_getSuperclass(currentClass))); + + // Add the selector as being modified. + currentClass = klass; + AspectTracker *subclassTracker = nil; + do { + tracker = swizzledClassesDict[currentClass]; + if (!tracker) { + tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass]; + swizzledClassesDict[(id)currentClass] = tracker; + } + if (subclassTracker) { + [tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName]; + } else { + [tracker.selectorNames addObject:selectorName]; + } + + // All superclasses get marked as having a subclass that is modified. + subclassTracker = tracker; + }while ((currentClass = class_getSuperclass(currentClass))); + } else { + return YES; + } + + return YES; +} + +static void aspect_deregisterTrackedSelector(id self, SEL selector) { + if (!class_isMetaClass(object_getClass(self))) return; + + NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict(); + NSString *selectorName = NSStringFromSelector(selector); + Class currentClass = [self class]; + AspectTracker *subclassTracker = nil; + do { + AspectTracker *tracker = swizzledClassesDict[currentClass]; + if (subclassTracker) { + [tracker removeSubclassTracker:subclassTracker hookingSelectorName:selectorName]; + } else { + [tracker.selectorNames removeObject:selectorName]; + } + if (tracker.selectorNames.count == 0 && tracker.selectorNamesToSubclassTrackers) { + [swizzledClassesDict removeObjectForKey:currentClass]; + } + subclassTracker = tracker; + }while ((currentClass = class_getSuperclass(currentClass))); +} + +@end + +@implementation AspectTracker + +- (id)initWithTrackedClass:(Class)trackedClass { + if (self = [super init]) { + _trackedClass = trackedClass; + _selectorNames = [NSMutableSet new]; + _selectorNamesToSubclassTrackers = [NSMutableDictionary new]; + } + return self; +} + +- (BOOL)subclassHasHookedSelectorName:(NSString *)selectorName { + return self.selectorNamesToSubclassTrackers[selectorName] != nil; +} + +- (void)addSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName { + NSMutableSet *trackerSet = self.selectorNamesToSubclassTrackers[selectorName]; + if (!trackerSet) { + trackerSet = [NSMutableSet new]; + self.selectorNamesToSubclassTrackers[selectorName] = trackerSet; + } + [trackerSet addObject:subclassTracker]; +} +- (void)removeSubclassTracker:(AspectTracker *)subclassTracker hookingSelectorName:(NSString *)selectorName { + NSMutableSet *trackerSet = self.selectorNamesToSubclassTrackers[selectorName]; + [trackerSet removeObject:subclassTracker]; + if (trackerSet.count == 0) { + [self.selectorNamesToSubclassTrackers removeObjectForKey:selectorName]; + } +} +- (NSSet *)subclassTrackersHookingSelectorName:(NSString *)selectorName { + NSMutableSet *hookingSubclassTrackers = [NSMutableSet new]; + for (AspectTracker *tracker in self.selectorNamesToSubclassTrackers[selectorName]) { + if ([tracker.selectorNames containsObject:selectorName]) { + [hookingSubclassTrackers addObject:tracker]; + } + [hookingSubclassTrackers unionSet:[tracker subclassTrackersHookingSelectorName:selectorName]]; + } + return hookingSubclassTrackers; +} +- (NSString *)trackedClassName { + return NSStringFromClass(self.trackedClass); +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %@, trackedClass: %@, selectorNames:%@, subclass selector names: %@>", self.class, self, NSStringFromClass(self.trackedClass), self.selectorNames, self.selectorNamesToSubclassTrackers.allKeys]; +} + +@end + +/////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - NSInvocation (Aspects) + +@implementation NSInvocation (Aspects) + +// Thanks to the ReactiveCocoa team for providing a generic solution for this. +- (id)aspect_argumentAtIndex:(NSUInteger)index { + const char *argType = [self.methodSignature getArgumentTypeAtIndex:index]; + // Skip const type qualifier. + if (argType[0] == _C_CONST) argType++; + +#define WRAP_AND_RETURN(type) do { type val = 0; [self getArgument:&val atIndex:(NSInteger)index]; return @(val); } while (0) + if (strcmp(argType, @encode(id)) == 0 || strcmp(argType, @encode(Class)) == 0) { + __autoreleasing id returnObj; + [self getArgument:&returnObj atIndex:(NSInteger)index]; + return returnObj; + } else if (strcmp(argType, @encode(SEL)) == 0) { + SEL selector = 0; + [self getArgument:&selector atIndex:(NSInteger)index]; + return NSStringFromSelector(selector); + } else if (strcmp(argType, @encode(Class)) == 0) { + __autoreleasing Class theClass = Nil; + [self getArgument:&theClass atIndex:(NSInteger)index]; + return theClass; + // Using this list will box the number with the appropriate constructor, instead of the generic NSValue. + } else if (strcmp(argType, @encode(char)) == 0) { + WRAP_AND_RETURN(char); + } else if (strcmp(argType, @encode(int)) == 0) { + WRAP_AND_RETURN(int); + } else if (strcmp(argType, @encode(short)) == 0) { + WRAP_AND_RETURN(short); + } else if (strcmp(argType, @encode(long)) == 0) { + WRAP_AND_RETURN(long); + } else if (strcmp(argType, @encode(long long)) == 0) { + WRAP_AND_RETURN(long long); + } else if (strcmp(argType, @encode(unsigned char)) == 0) { + WRAP_AND_RETURN(unsigned char); + } else if (strcmp(argType, @encode(unsigned int)) == 0) { + WRAP_AND_RETURN(unsigned int); + } else if (strcmp(argType, @encode(unsigned short)) == 0) { + WRAP_AND_RETURN(unsigned short); + } else if (strcmp(argType, @encode(unsigned long)) == 0) { + WRAP_AND_RETURN(unsigned long); + } else if (strcmp(argType, @encode(unsigned long long)) == 0) { + WRAP_AND_RETURN(unsigned long long); + } else if (strcmp(argType, @encode(float)) == 0) { + WRAP_AND_RETURN(float); + } else if (strcmp(argType, @encode(double)) == 0) { + WRAP_AND_RETURN(double); + } else if (strcmp(argType, @encode(BOOL)) == 0) { + WRAP_AND_RETURN(BOOL); + } else if (strcmp(argType, @encode(bool)) == 0) { + WRAP_AND_RETURN(BOOL); + } else if (strcmp(argType, @encode(char *)) == 0) { + WRAP_AND_RETURN(const char *); + } else if (strcmp(argType, @encode(void (^)(void))) == 0) { + __unsafe_unretained id block = nil; + [self getArgument:&block atIndex:(NSInteger)index]; + return [block copy]; + } else { + NSUInteger valueSize = 0; + NSGetSizeAndAlignment(argType, &valueSize, NULL); + + unsigned char valueBytes[valueSize]; + [self getArgument:valueBytes atIndex:(NSInteger)index]; + + return [NSValue valueWithBytes:valueBytes objCType:argType]; + } + return nil; +#undef WRAP_AND_RETURN +} + +- (NSArray *)aspects_arguments { + NSMutableArray *argumentsArray = [NSMutableArray array]; + for (NSUInteger idx = 2; idx < self.methodSignature.numberOfArguments; idx++) { + [argumentsArray addObject:[self aspect_argumentAtIndex:idx] ?: NSNull.null]; + } + return [argumentsArray copy]; +} + +@end + +/////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - AspectIdentifier + +@implementation AspectIdentifier + ++ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error { + NSCParameterAssert(block); + NSCParameterAssert(selector); + NSMethodSignature *blockSignature = aspect_blockMethodSignature(block, error); // TODO: check signature compatibility, etc. + if (!aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) { + return nil; + } + + AspectIdentifier *identifier = nil; + if (blockSignature) { + identifier = [AspectIdentifier new]; + identifier.selector = selector; + identifier.block = block; + identifier.blockSignature = blockSignature; + identifier.options = options; + identifier.object = object; // weak + } + return identifier; +} + +- (BOOL)invokeWithInfo:(id)info { + NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature]; + NSInvocation *originalInvocation = info.originalInvocation; + NSUInteger numberOfArguments = self.blockSignature.numberOfArguments; + + // Be extra paranoid. We already check that on hook registration. + if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) { + AspectLogError(@"Block has too many arguments. Not calling %@", info); + return NO; + } + + // The `self` of the block will be the AspectInfo. Optional. + if (numberOfArguments > 1) { + [blockInvocation setArgument:&info atIndex:1]; + } + + void *argBuf = NULL; + for (NSUInteger idx = 2; idx < numberOfArguments; idx++) { + const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx]; + NSUInteger argSize; + NSGetSizeAndAlignment(type, &argSize, NULL); + + if (!(argBuf = reallocf(argBuf, argSize))) { + AspectLogError(@"Failed to allocate memory for block invocation."); + return NO; + } + + [originalInvocation getArgument:argBuf atIndex:idx]; + [blockInvocation setArgument:argBuf atIndex:idx]; + } + + [blockInvocation invokeWithTarget:self.block]; + + if (argBuf != NULL) { + free(argBuf); + } + return YES; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, SEL:%@ object:%@ options:%tu block:%@ (#%tu args)>", self.class, self, NSStringFromSelector(self.selector), self.object, self.options, self.block, self.blockSignature.numberOfArguments]; +} + +- (BOOL)remove { + return aspect_remove(self, NULL); +} + +@end + +/////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - AspectsContainer + +@implementation AspectsContainer + +- (BOOL)hasAspects { + return self.beforeAspects.count > 0 || self.insteadAspects.count > 0 || self.afterAspects.count > 0; +} + +- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options { + NSParameterAssert(aspect); + NSUInteger position = options&AspectPositionFilter; + switch (position) { + case AspectPositionBefore: self.beforeAspects = [(self.beforeAspects ?:@[]) arrayByAddingObject:aspect]; break; + case AspectPositionInstead: self.insteadAspects = [(self.insteadAspects?:@[]) arrayByAddingObject:aspect]; break; + case AspectPositionAfter: self.afterAspects = [(self.afterAspects ?:@[]) arrayByAddingObject:aspect]; break; + } +} + +- (BOOL)removeAspect:(id)aspect { + for (NSString *aspectArrayName in @[NSStringFromSelector(@selector(beforeAspects)), + NSStringFromSelector(@selector(insteadAspects)), + NSStringFromSelector(@selector(afterAspects))]) { + NSArray *array = [self valueForKey:aspectArrayName]; + NSUInteger index = [array indexOfObjectIdenticalTo:aspect]; + if (array && index != NSNotFound) { + NSMutableArray *newArray = [NSMutableArray arrayWithArray:array]; + [newArray removeObjectAtIndex:index]; + [self setValue:newArray forKey:aspectArrayName]; + return YES; + } + } + return NO; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, before:%@, instead:%@, after:%@>", self.class, self, self.beforeAspects, self.insteadAspects, self.afterAspects]; +} + +@end + +/////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - AspectInfo + +@implementation AspectInfo + +@synthesize arguments = _arguments; + +- (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation { + NSCParameterAssert(instance); + NSCParameterAssert(invocation); + if (self = [super init]) { + _instance = instance; + _originalInvocation = invocation; + } + return self; +} + +- (NSArray *)arguments { + // Lazily evaluate arguments, boxing is expensive. + if (!_arguments) { + _arguments = self.originalInvocation.aspects_arguments; + } + return _arguments; +} + +@end diff --git a/qmuidemo/Modules/Debug/NSObject+QDSafe.h b/qmuidemo/Modules/Debug/NSObject+QDSafe.h new file mode 100644 index 00000000..ad4d5eae --- /dev/null +++ b/qmuidemo/Modules/Debug/NSObject+QDSafe.h @@ -0,0 +1,17 @@ +// +// NSObject+QDSafe.h +// qmuidemo +// +// Created by ziezheng on 2020/6/12. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSObject (QDSafe) + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Debug/NSObject+QDSafe.m b/qmuidemo/Modules/Debug/NSObject+QDSafe.m new file mode 100644 index 00000000..31e55113 --- /dev/null +++ b/qmuidemo/Modules/Debug/NSObject+QDSafe.m @@ -0,0 +1,83 @@ +// +// NSObject+QDSafe.m +// qmuidemo +// +// Created by ziezheng on 2020/6/12. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "NSObject+QDSafe.h" + +@implementation NSObject (QDSafe) + +void swizzleInstanceMethod(Class cls, SEL origSelector, SEL newSelector) +{ + if (!cls) { + return; + } + /* if current class not exist selector, then get super*/ + Method originalMethod = class_getInstanceMethod(cls, origSelector); + Method swizzledMethod = class_getInstanceMethod(cls, newSelector); + + /* add selector if not exist, implement append with method */ + if (class_addMethod(cls, + origSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod)) ) { + /* replace class instance method, added if selector not exist */ + /* for class cluster , it always add new selector here */ + class_replaceMethod(cls, + newSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod)); + + } else { + /* swizzleMethod maybe belong to super */ + class_replaceMethod(cls, + newSelector, + class_replaceMethod(cls, + origSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod)), + method_getTypeEncoding(originalMethod)); + } +} + ++ (void)load2 +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + swizzleInstanceMethod([NSObject class], @selector(methodSignatureForSelector:), @selector(hookMethodSignatureForSelector:)); + swizzleInstanceMethod([NSObject class], @selector(forwardInvocation:), @selector(hookForwardInvocation:)); + }); +} + +- (NSMethodSignature*)hookMethodSignatureForSelector:(SEL)aSelector { + /* 如果 当前类有methodSignatureForSelector实现,NSObject的实现直接返回nil + * 子类实现如下: + * NSMethodSignature* sig = [super methodSignatureForSelector:aSelector]; + * if (!sig) { + * //当前类的methodSignatureForSelector实现 + * //如果当前类的methodSignatureForSelector也返回nil + * } + * return sig; + */ + NSMethodSignature* sig = [self hookMethodSignatureForSelector:aSelector]; + if (!sig){ + if (class_getMethodImplementation([NSObject class], @selector(methodSignatureForSelector:)) + != class_getMethodImplementation(self.class, @selector(methodSignatureForSelector:)) ){ + return nil; + } + return [NSMethodSignature signatureWithObjCTypes:"v@:@"]; + } + return sig; +} + +- (void)hookForwardInvocation:(NSInvocation*)invocation +{ + NSString* info = [NSString stringWithFormat:@"unrecognized selector [%@] sent to %@", NSStringFromSelector(invocation.selector), NSStringFromClass(self.class)]; + NSLog(@"%@",info); +} + +@end diff --git a/qmuidemo/Modules/Debug/NSObject+QMUIHook.h b/qmuidemo/Modules/Debug/NSObject+QMUIHook.h new file mode 100644 index 00000000..019941b0 --- /dev/null +++ b/qmuidemo/Modules/Debug/NSObject+QMUIHook.h @@ -0,0 +1,53 @@ +// +// NSObject+QMUIHook.h +// qmuidemo +// +// Created by ziezheng on 2020/6/12. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_OPTIONS(NSUInteger, QMUIHookOption) { + QMUIHookOptionBefore = 0, + QMUIHookOptionAfter = 1, + QMUIHookOptionInstead = 2, +}; + +@interface QMUIHookContext : NSObject + +@property(nonatomic, weak) NSObject *instance; + +- (void)getReturnValue:(void *)retLoc; +- (void)setReturnValue:(void *)retLoc; + +- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx; +- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx; + +- (void)invoke; + +@end + +@interface QMUIHookToken : NSObject + +@property (nonatomic, assign) SEL selector; +@property (nonatomic, strong) id block; +//@property (nonatomic, strong) NSMethodSignature *blockSignature; +@property (nonatomic, weak) id object; +@property (nonatomic, assign) QMUIHookOption options; + +@end + +@interface NSObject (QMUIHook) + +typedef void (^QMUIHookContextBlock)(QMUIHookContext *context); + +- (QMUIHookToken *)qmui_hookSelector:(SEL)selector beforeBlock:(QMUIHookContextBlock)block; +- (QMUIHookToken *)qmui_hookSelector:(SEL)selector afterBlock:(QMUIHookContextBlock)block; +- (QMUIHookToken *)qmui_hookSelector:(SEL)selector insteadBlock:(QMUIHookContextBlock)block; + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Debug/NSObject+QMUIHook.m b/qmuidemo/Modules/Debug/NSObject+QMUIHook.m new file mode 100644 index 00000000..74c40043 --- /dev/null +++ b/qmuidemo/Modules/Debug/NSObject+QMUIHook.m @@ -0,0 +1,542 @@ +// +// NSObject+QMUIHook.m +// qmuidemo +// +// Created by ziezheng on 2020/6/12. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "NSObject+QMUIHook.h" +#import + +@interface QMUIHookContext() +@property(nonatomic, strong) NSInvocation *originalInvocation; +@property(nonatomic, copy) dispatch_block_t invokeBlock; +@property(nonatomic, copy) void (^getReturnValueBlock)(void *retLoc); +@property(nonatomic, copy) void (^setReturnValueBlock)(void *retLoc); +@property(nonatomic, copy) void (^getArgumentBlock)(void *argumentLocation, NSInteger idx); +@property(nonatomic, copy) void (^setArgumentBlock)(void *argumentLocation, NSInteger idx); +@end + +@implementation QMUIHookContext + ++ (instancetype)contextWithInstance:(id)instance invocation:(NSInvocation *)invocation { + QMUIHookContext *context = [[self alloc] init]; + context.instance = instance; + context.originalInvocation = invocation; + return context; +} + +- (void)getReturnValue:(void *)retLoc { + if (self.originalInvocation) { + [self.originalInvocation getReturnValue:retLoc]; + } else if (self.getReturnValueBlock) { + self.getReturnValueBlock(retLoc); + } +} +- (void)setReturnValue:(void *)retLoc { + if (self.originalInvocation) { + [self.originalInvocation setReturnValue:retLoc]; + } else if (self.setReturnValueBlock) { + self.setReturnValueBlock(retLoc); + } +} + +- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx { + if (self.originalInvocation) { + [self.originalInvocation getArgument:argumentLocation atIndex:idx]; + } else if (self.getArgumentBlock) { + self.getArgumentBlock(argumentLocation, idx); + } +} +- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx { + if (self.originalInvocation) { + [self.originalInvocation setArgument:argumentLocation atIndex:idx]; + } else if (self.setArgumentBlock) { + self.setArgumentBlock(argumentLocation, idx); + } +} + +- (void)invoke { + if (self.invokeBlock) { + self.invokeBlock(); + } +} + +@end + + +@implementation QMUIHookToken + ++ (instancetype)tokenWithSelector:(SEL)selector object:(id)object options:(QMUIHookOption)options block:(id)block { + NSCParameterAssert(block); + NSCParameterAssert(selector); + QMUIHookToken *token = [[QMUIHookToken alloc] init]; + token.selector = selector; + token.block = block; + token.options = options; + token.object = object; + return token; +} + +@end + + +@interface QMUIHookTokenContainer : NSObject + +- (void)addToken:(QMUIHookToken *)token withOptions:(QMUIHookOption)hookOption; +- (void)removeToken:(QMUIHookToken *)token; +- (BOOL)hasTokens; + +@property(nonatomic, strong) NSMethodSignature *methodSignature; +@property (atomic, copy) NSArray *beforeTokens; +@property (atomic, copy) NSArray *afterTokens; +@property (atomic, copy) NSArray *insteadTokens; + +@end + +@implementation QMUIHookTokenContainer + +- (void)addToken:(QMUIHookToken *)token withOptions:(QMUIHookOption)hookOption { + switch (hookOption) { + case QMUIHookOptionBefore: + self.beforeTokens = [(self.beforeTokens ? : @[]) arrayByAddingObject:token]; + break; + case QMUIHookOptionAfter: + self.afterTokens = [(self.afterTokens ? : @[]) arrayByAddingObject:token]; + break; + case QMUIHookOptionInstead: + self.insteadTokens = [(self.insteadTokens ? : @[]) arrayByAddingObject:token]; + break; + } +} + +- (void)removeToken:(QMUIHookToken *)token { + switch (token.options) { + case QMUIHookOptionBefore: { + NSMutableArray *beforeTokens = [self.beforeTokens mutableCopy]; + [beforeTokens removeObject:token]; + self.beforeTokens = beforeTokens.copy; + } + break; + case QMUIHookOptionAfter: { + NSMutableArray *afterTokens = [self.afterTokens mutableCopy]; + [afterTokens removeObject:token]; + self.afterTokens = afterTokens.copy; + } + break; + case QMUIHookOptionInstead: { + NSMutableArray *insteadTokens = [self.insteadTokens mutableCopy]; + [insteadTokens removeObject:token]; + self.insteadTokens = insteadTokens.copy; + } + break; + } +} + +- (BOOL)hasTokens { + return self.beforeTokens.count > 0 || self.insteadTokens.count > 0 || self.afterTokens.count > 0; +} + +@end + + +@interface QMUIHookTemple : NSObject + ++ (BOOL)hasTempleWithMethodSignature:(NSString *)methodSignature; + ++ (id)getOverrideImplementationBlockWithOriginalIMPProvider:(IMP (^)(void))originalIMPProvider selector:(SEL)selector methodSignature:(NSString *)methodSignature; + +@end + +@implementation NSObject (QMUIHook) + +static void __QMUIDidHookClass(Class class, SEL selector); +static NSString * __QMUIHookSelectorNameRecognizer(NSString *oriSelectorName); + +static IMP qmui_getMsgForwardIMP(Class class, SEL selector) { + IMP msgForwardIMP = _objc_msgForward; + #if !defined(__arm64__) + Method method = class_getInstanceMethod(class, selector); + const char *typeDescription = method_getTypeEncoding(method); + if (typeDescription[0] == '{') { + // 以下代码参考 JSPatch 的实现: + //In some cases that returns struct, we should use the '_stret' API: + //http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html + //NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription. + NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:typeDescription]; + if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) { + msgForwardIMP = (IMP)_objc_msgForward_stret; + } + } + #endif + return msgForwardIMP; +} + +static SEL QMUI_backupSelectorFromOriginSelector(Class class, SEL originSel) { + NSString *originSelName = NSStringFromSelector(originSel); + NSString *originClassName = NSStringFromClass(class); + if (!originSelName || !originClassName) { + // 未注册过的 selector 是拿不到 String 的 + return NULL; + } + return NSSelectorFromString([NSString stringWithFormat:@"qmui_backup_%@_%@", originClassName , originSelName]); +} + +static QMUIHookTokenContainer *QMUI_getContainerForObject(NSObject *self, SEL selector, BOOL create) { + NSCParameterAssert(self); + NSString *key = [NSString stringWithFormat:@"QMUIHookContainerKey_%@", NSStringFromSelector(selector)]; + QMUIHookTokenContainer *container = [self qmui_getBoundObjectForKey:key]; + if (!container && create) { + container = [QMUIHookTokenContainer new]; + container.methodSignature = [[self class] instanceMethodSignatureForSelector:selector]; + [self qmui_bindObject:container forKey:key]; + } + return container; +} + + +static SEL QMUI_originalSelectorFromForwardInvocation(NSInvocation *invocation) { + NSString *originalSelectorName = __QMUIHookSelectorNameRecognizer(NSStringFromSelector(invocation.selector)); + return NSSelectorFromString(originalSelectorName); +} + +CG_INLINE void QMUI_hookedMethodWillCall(QMUIHookTokenContainer *container, QMUIHookContext *context) { + if (!container) return; + for (QMUIHookToken *toekn in container.beforeTokens) { + QMUIHookContextBlock block = toekn.block; + block(context); + } +} + +CG_INLINE void QMUI_hookedMethodCall(QMUIHookTokenContainer *container, QMUIHookContext *context, dispatch_block_t oriInvokeBlock) { + if (container.insteadTokens > 0) { + context.invokeBlock = oriInvokeBlock; + for (QMUIHookToken *toekn in container.insteadTokens) { + QMUIHookContextBlock block = toekn.block; + block(context); + } + } else { + oriInvokeBlock(); + } +} + +CG_INLINE void QMUI_hookedMethodDidCall(QMUIHookTokenContainer *container, QMUIHookContext *context) { + if (!container) return; + for (QMUIHookToken *toekn in container.afterTokens) { + QMUIHookContextBlock block = toekn.block; + block(context); + } +} + +static void QMUIForwardInvocation(__unsafe_unretained id self, SEL selector, NSInvocation *invocation) { +// NSLog(@"💎 QMUIForwardInvocation! 111 %@", NSStringFromSelector(invocation.selector)); + IMP (^originalIMPProvider)(void) = nil; + Class forwardingClass = object_getClass(self); + while (forwardingClass) { + originalIMPProvider = [(id)forwardingClass qmui_getBoundObjectForKey:@"forwardInvocationOriginalIMPProvider"]; + if (originalIMPProvider) break; + // 走到这里说明当前实例对象被使用了 object_setClass 修改 isa,导致 class 找不到原始的 forwardInvocation,此时应该用向 super class 寻找(比如先被 QMUI hook 再被 aspect hook) + forwardingClass = class_getSuperclass(forwardingClass); + } + + SEL originalSelector = QMUI_originalSelectorFromForwardInvocation(invocation); + QMUIHookTokenContainer *container = QMUI_getContainerForObject(self, originalSelector, NO); + QMUIHookContext *context = nil; + if (container) { + context = [QMUIHookContext contextWithInstance:self invocation:invocation]; + } + + SEL backupSelector = QMUI_backupSelectorFromOriginSelector(forwardingClass, originalSelector); + IMP imp = class_getMethodImplementation(forwardingClass, backupSelector); + dispatch_block_t invokeBlock = nil; + if (imp == qmui_getMsgForwardIMP(forwardingClass, selector)) { + // 来到这有两个可能: + // 1. 业务本身没有实现该方法,但是重写了 reponse:可以响应该方法 + // 2. 先使用了 aspect hook 掉这个方法,然后再被 QMUI hook + // 上述两种都需要调用被 QMUI hook 之前保存的那个 forwardInvocation: ,由原始的 forwardInvocation: 去处理 + invokeBlock = ^{ + ((void (*)(id, SEL, id))originalIMPProvider())(invocation.target, selector, invocation); + }; + } else { + // 使用 backupSelector 调用原始实现,这里会修改 _cmd + invocation.selector = backupSelector; + invokeBlock = ^{ + [invocation invoke]; + }; + } + QMUI_hookedMethodWillCall(container, context); + QMUI_hookedMethodCall(container, context, invokeBlock); + QMUI_hookedMethodDidCall(container, context); +} + +static void QMUI_swizzleForwardInvocation(Class class) { + [QMUIHelper executeBlock:^{ + OverrideImplementation(class, @selector(forwardInvocation:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + [class qmui_bindObject:originalIMPProvider forKey:@"forwardInvocationOriginalIMPProvider"]; + return ^(id selfObject, NSInvocation *invocation) { + QMUIForwardInvocation(selfObject, originCMD, invocation); + }; + }); + } oncePerIdentifier:[NSString stringWithFormat:@"QMUI_swizzleForwardInvocation_%@", NSStringFromClass(class)]]; +} + +static bool QMUI_isClassHooked(Class class, SEL selector) { + SEL backupSelector = QMUI_backupSelectorFromOriginSelector(class, selector); + return HasOverrideSuperclassMethod(class, backupSelector); +} + +static bool QMUI_hookClass(Class class, SEL selector) { + // 1. swizzle Class 的 forwardInvocation: + QMUI_swizzleForwardInvocation(class); + // 2. 保存原始 IMP + SEL backupSelector = QMUI_backupSelectorFromOriginSelector(class, selector); + const char * typeEncoding = (char *)method_getTypeEncoding(class_getInstanceMethod(class, selector)); + if(!HasOverrideSuperclassMethod(class, backupSelector)) { + IMP originalImp = class_getMethodImplementation(class, selector); + class_addMethod(class, backupSelector, originalImp, typeEncoding); + } + // 3.把 selector 指向 _objc_msgForward + IMP msgForwardIMP = qmui_getMsgForwardIMP(class, selector); + class_replaceMethod(class, selector, msgForwardIMP, typeEncoding); + __QMUIDidHookClass(class, selector); + return true; +} + +- (NSString *)typeEncodingStringByRemovingDigit:(const char *)typeEncoding { + size_t strLength = strlen(typeEncoding); + NSMutableString *typeEncodingString = [NSMutableString stringWithCapacity:strLength]; + for (NSInteger i = 0; i < strLength; i++) { + unichar c = typeEncoding[i]; + if (!isdigit(c)) { + [typeEncodingString appendString:[NSString stringWithCharacters:&c length:1]]; + } + } + return typeEncodingString; +} + +- (void)qmui_hookClassWithSelector:(SEL)selector { + Class statedClass = self.class; + const char *typeEncoding = (char *)method_getTypeEncoding(class_getInstanceMethod(statedClass, selector)); + NSString *qmuiSignature = [self typeEncodingStringByRemovingDigit:typeEncoding]; + if ([QMUIHookTemple hasTempleWithMethodSignature:qmuiSignature]) { + // 有模板用模板 hook + // TODO: Aspect Hook 后用模板 HOOK 只能生效一次 + NSString *key = [NSString stringWithFormat:@"%@_%@", NSStringFromClass(statedClass), NSStringFromSelector(selector)]; + [QMUIHelper executeBlock:^{ + OverrideImplementation(statedClass, selector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return [QMUIHookTemple getOverrideImplementationBlockWithOriginalIMPProvider:originalIMPProvider selector:originCMD methodSignature:qmuiSignature]; + }); + } oncePerIdentifier:key]; + return; + } + + // 没有模板 用消息转发 + if (!QMUI_isClassHooked(statedClass, selector)) { + QMUI_hookClass(statedClass, selector); + Class realClass = object_getClass(self); + if (realClass != statedClass) { + BOOL isKVOing = [NSStringFromClass(realClass) hasPrefix:@"NSKVONotifying_"]; + BOOL shouldHookRealClass = !isKVOing; + if (shouldHookRealClass) { + // 一般请下 realClass == statedClass,除非使用 object_setClass 改变 isa,这里为了兼容 Aspect,也需要 hook realClass(也就是带有 _Aspects_ 前缀的类) + // 如果是 KVO 对象,则不能 hook realClass,否则 _NSSetObjectValueAndNotify 调用时 _cmd 被提前改变,会使用 KVO 失效 + // Aspect 本身不兼容 KVO ,所以一个对象如果被 KVO 了就不用去考虑兼容 Aspect 了 + QMUI_hookClass(realClass, selector); + } + } + } +} + +- (QMUIHookToken *)qmui_hookSelector:(SEL)selector beforeBlock:(QMUIHookContextBlock)block { + return [self qmui_hookSelector:selector withOption:QMUIHookOptionBefore block:block]; +} +- (QMUIHookToken *)qmui_hookSelector:(SEL)selector afterBlock:(QMUIHookContextBlock)block { + return [self qmui_hookSelector:selector withOption:QMUIHookOptionAfter block:block]; +} + +- (QMUIHookToken *)qmui_hookSelector:(SEL)selector insteadBlock:(QMUIHookContextBlock)block { + return [self qmui_hookSelector:selector withOption:QMUIHookOptionInstead block:block]; +} + +- (QMUIHookToken *)qmui_hookSelector:(SEL)selector withOption:(QMUIHookOption)option block:(QMUIHookContextBlock)block{ + if (![self respondsToSelector:selector]) { + return nil; + } + [self qmui_hookClassWithSelector:selector]; + QMUIHookToken *token = [QMUIHookToken tokenWithSelector:selector object:self options:QMUIHookOptionAfter block:block]; + QMUIHookTokenContainer *container = QMUI_getContainerForObject(self, selector, YES); + [container addToken:token withOptions:option]; + return token; +} + + +@end + +#pragma mark - 兼容其他 Hook + +static NSString * const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:"; +static NSString * const AspectsMessagePrefix = @"aspects__"; + +static void __QMUIDidHookClass(Class class, SEL selector) { + class_addMethod(class, NSSelectorFromString(AspectsForwardInvocationSelectorName), (IMP)QMUIForwardInvocation, "v@:@@"); + SEL aliasSelector = NSSelectorFromString([AspectsMessagePrefix stringByAppendingFormat:@"%@", NSStringFromSelector(selector)]); + if (![class instancesRespondToSelector:aliasSelector]) { + const char * typeEncoding = (char *)method_getTypeEncoding(class_getInstanceMethod(class, selector)); + class_addMethod(class, aliasSelector, qmui_getMsgForwardIMP(class, selector), typeEncoding); + } +} + +static NSString * __QMUIHookSelectorNameRecognizer(NSString *oriSelectorName) { + if ([oriSelectorName hasPrefix:AspectsMessagePrefix]) { + return [oriSelectorName stringByReplacingOccurrencesOfString:AspectsMessagePrefix withString:@""]; + } + return oriSelectorName; +} + + +#pragma mark - Template + + +@implementation QMUIHookTemple + ++ (BOOL)hasTempleWithMethodSignature:(NSString *)methodSignature { + NSSet *set = [NSSet setWithObjects:@"v@:", @"vB:", @"@@:", @"v@:@", @"@@:@", nil]; + return !![set containsObject:methodSignature]; +} + +#define QMUIVoidArg1(_Type1) ^(id selfObject, _Type1 arg1) {\ + QMUIHookTokenContainer *container = QMUI_getContainerForObject(selfObject, selector, NO);\ + if (!container) {\ + return ((void (*)(id, SEL, _Type1))originalIMPProvider())(selfObject, selector, arg1);\ + }\ + __block id _selfObject = selfObject;\ + __block SEL _selector = selector;\ + __block _Type1 _arg1 = arg1;\ + QMUIHookContext *context = nil;\ + [self setInalInvocationBlockForContext:&context container:container selfObject:_selfObject selfObjectPtr:&_selfObject selector:_selector selectorPtr:&_selector returnPtr:NULL arg1:&_arg1];\ + QMUI_hookedMethodWillCall(container, context);\ + QMUI_hookedMethodCall(container, context, ^{\ + ((void (*)(id, SEL, _Type1))originalIMPProvider())(_selfObject, _selector, _arg1);\ + });\ + QMUI_hookedMethodDidCall(container, context);\ +};\ + +#define QMUIRutureTypeArg0(_reruenType) ^id(id selfObject) { \ + QMUIHookTokenContainer *container = QMUI_getContainerForObject(selfObject, selector, NO);\ + if (!container) {\ + return ((id (*)(_reruenType, SEL))originalIMPProvider())(selfObject, selector);\ + }\ + __block id _selfObject = selfObject;\ + __block SEL _selector = selector;\ + __block _reruenType _returnValue = nil;\ + QMUIHookContext *context = nil;\ + [self setInalInvocationBlockForContext:&context container:container selfObject:_selfObject selfObjectPtr:&_selfObject selector:_selector selectorPtr:&_selector returnPtr:&_returnValue arg1:NULL];\ + QMUI_hookedMethodWillCall(container, context);\ + QMUI_hookedMethodCall(container, context, ^{\ + _returnValue = ((_reruenType (*)(id, SEL))originalIMPProvider())(_selfObject, _selector);\ + });\ + QMUI_hookedMethodDidCall(container, context);\ + return _returnValue;\ +};\ + +#define QMUIRutureTypeArg1(_reruenType, _Type1) ^id(id selfObject, _Type1 arg1) { \ + QMUIHookTokenContainer *container = QMUI_getContainerForObject(selfObject, selector, NO);\ + if (!container) {\ + return ((id (*)(_reruenType, SEL, _Type1))originalIMPProvider())(selfObject, selector, arg1);\ + }\ + __block id _selfObject = selfObject;\ + __block SEL _selector = selector;\ + __block _Type1 _arg1 = arg1;\ + __block _reruenType _returnValue = nil;\ + QMUIHookContext *context = nil;\ + [self setInalInvocationBlockForContext:&context container:container selfObject:_selfObject selfObjectPtr:&_selfObject selector:_selector selectorPtr:&_selector returnPtr:&_returnValue arg1:&_arg1];\ + QMUI_hookedMethodWillCall(container, context);\ + QMUI_hookedMethodCall(container, context, ^{\ + _returnValue = ((_reruenType (*)(id, SEL, _Type1))originalIMPProvider())(_selfObject, _selector, _arg1);\ + });\ + QMUI_hookedMethodDidCall(container, context);\ + return _returnValue;\ +};\ + ++ (void)setInalInvocationBlockForContext:(QMUIHookContext **)contextPtr container:(QMUIHookTokenContainer *)container selfObject:(id)selfObject selfObjectPtr:(void *)selfObjectPtr selector:(SEL)selector selectorPtr:(void *)selectorPtr returnPtr:(void *)retValuePtr arg1:(void *)arg1Ptr { + + QMUIHookContext *context = [QMUIHookContext contextWithInstance:nil invocation:nil]; + context.getArgumentBlock = ^(void *argumentLocation, NSInteger idx) { + if (!argumentLocation || idx >= container.methodSignature.numberOfArguments) { + return; + } + const char *type = [container.methodSignature getArgumentTypeAtIndex:idx]; + NSUInteger argSize; + NSGetSizeAndAlignment(type, &argSize, NULL); + if (idx == 0) { + memcpy(argumentLocation, selfObjectPtr, argSize); + } else if (idx == 1) { + memcpy(argumentLocation, selectorPtr, argSize); + } else if (idx == 2 && arg1Ptr) { + memcpy(argumentLocation, arg1Ptr, argSize); + } + }; + context.setArgumentBlock = ^(void *argumentLocation, NSInteger idx) { + if (!argumentLocation || idx >= container.methodSignature.numberOfArguments) { + return; + } + const char *type = [container.methodSignature getArgumentTypeAtIndex:idx]; + NSUInteger argSize; + NSGetSizeAndAlignment(type, &argSize, NULL); + if (idx == 0) { + memcpy(selfObjectPtr, argumentLocation, argSize); + } else if (idx == 1) { + memcpy(selectorPtr, argumentLocation, argSize); + } else if (idx == 2 && arg1Ptr) { + memcpy(arg1Ptr, argumentLocation, argSize); + } + }; + if (retValuePtr) { + context.setReturnValueBlock = ^(void *retLoc) { + memcpy(retValuePtr, retLoc, container.methodSignature.methodReturnLength); + }; + context.getReturnValueBlock = ^(void *retLoc) { + memcpy(retLoc, retValuePtr, container.methodSignature.methodReturnLength); + }; + } + *contextPtr = context; +} + ++ (id)getOverrideImplementationBlockWithOriginalIMPProvider:(IMP (^)(void))originalIMPProvider selector:(SEL)selector methodSignature:(NSString *)methodSignature { + // void + if ([methodSignature isEqualToString:@"v@:"]) { // - (void)func; + id block = ^(id selfObject) { + QMUIHookTokenContainer *container = QMUI_getContainerForObject(selfObject, selector, NO); + if (!container) { + return ((void (*)(id, SEL))originalIMPProvider())(selfObject, selector); + } + __block id _selfObject = selfObject; + __block SEL _selector = selector; + QMUIHookContext *context = nil; + [self setInalInvocationBlockForContext:&context container:container selfObject:_selfObject selfObjectPtr:&_selfObject selector:_selector selectorPtr:&_selector returnPtr:NULL arg1:NULL]; + QMUI_hookedMethodWillCall(container, context); + QMUI_hookedMethodCall(container, context, ^{ + return ((void (*)(id, SEL))originalIMPProvider())(_selfObject, _selector); + }); + QMUI_hookedMethodDidCall(container, context); + }; + return block; + } + // void & single arg + else if ([methodSignature isEqualToString:@"v@:B"]) { // - (void)setBool:(BOOL)bool; + return QMUIVoidArg1(BOOL); + } + else if ([methodSignature isEqualToString:@"v@:@"]) { // - (void)setId:(id)id; + return QMUIVoidArg1(id); + } + // single return value + else if ([methodSignature isEqualToString:@"@@:"]) { // - (id)idValue; + return QMUIRutureTypeArg0(id); + } else if ([methodSignature isEqualToString:@"@@:@"]) { + return QMUIRutureTypeArg1(id, id); + } + return nil; +} + +@end diff --git a/qmuidemo/Modules/Debug/QDHookViewController.h b/qmuidemo/Modules/Debug/QDHookViewController.h new file mode 100644 index 00000000..bb8ff9a9 --- /dev/null +++ b/qmuidemo/Modules/Debug/QDHookViewController.h @@ -0,0 +1,17 @@ +// +// QDHookViewController.h +// qmuidemo +// +// Created by ziezheng on 2020/6/12. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDHookViewController : QDCommonViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Debug/QDHookViewController.m b/qmuidemo/Modules/Debug/QDHookViewController.m new file mode 100644 index 00000000..843fa7b4 --- /dev/null +++ b/qmuidemo/Modules/Debug/QDHookViewController.m @@ -0,0 +1,298 @@ +// +// QDHookViewController.m +// qmuidemo +// +// Created by ziezheng on 2020/6/12. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDHookViewController.h" +#import "Aspects.h" +#import "Aspects.h" +#import "NSObject+QMUIHook.h" + +@interface QDObjectA : NSObject +@property(nonatomic, strong) NSString *name; +@end + +@implementation QDObjectA + +- (void)func { + NSLog(@"QDObjectA func invoke!"); +} + +- (void)setName:(NSString *)name { + _name = name.copy; + NSLog(@"QDObjectA setName invoke! name = %@", name); + + +} + + +@end + +@interface QDObjectB : QDObjectA + +@end + +@implementation QDObjectB + + +- (void)func { + [super func]; + NSLog(@"QDObjectB func invoke!"); +} + + + +@end + + +@interface QDHookViewController () + +@end + +@implementation QDHookViewController + + +#define aspectBefore(obj, sel, block) [obj aspect_hookSelector:sel withOptions:AspectPositionBefore usingBlock:^(id aspectInfo) { \ + block;\ +} error:nil];\ + +#define aspectAfter(obj, sel, block) [obj aspect_hookSelector:sel withOptions:AspectPositionAfter usingBlock:^(id aspectInfo) { \ + block;\ +} error:nil];\ + +#define qmuiBefore(obj, sel, block) [obj qmui_hookSelector:sel beforeBlock:^(QMUIHookContext * _Nonnull context) {\ + block;\ +}];\ + +#define qmuiAfter(obj, sel, block) [obj qmui_hookSelector:sel afterBlock:^(QMUIHookContext * _Nonnull context) {\ + block;\ +}];\ + +- (void)baseSample1 { + + QDObjectB *obj1 = [QDObjectB new]; + qmuiBefore(obj1, @selector(setName:), NSLog(@"🎈qmui before !")) + qmuiAfter(obj1, @selector(setName:), NSLog(@"🎈qmui after !")) + obj1.name = @"obj1 !"; + + QDObjectB *obj2 = [QDObjectB new]; + obj2.name = @"obj2 !"; +} + +- (void)sampleKVO { + NSLog(@"🎈 %@", NSStringFromSelector(_cmd)); + // KVO hook 移除 KVO + QDObjectB *obj1 = [QDObjectB new]; + qmuiBefore(obj1, @selector(setName:), NSLog(@"🎈qmui before !")) + qmuiAfter(obj1, @selector(setName:), NSLog(@"🎈qmui after !")) + + obj1.name = @"before KVO"; + [obj1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL]; + obj1.name = @"after KVO"; + [obj1 removeObserver:self forKeyPath:@"name"]; + obj1.name = @"remove KVO"; + + + // KVO hook 移除 KVO + QDObjectB *obj2 = [QDObjectB new]; + [obj2 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL]; + qmuiBefore(obj2, @selector(setName:), NSLog(@"🎈qmui before !")) + qmuiAfter(obj2, @selector(setName:), NSLog(@"🎈qmui after !")) + obj2.name = @"after KVO 2"; + [obj2 removeObserver:self forKeyPath:@"name"]; + obj2.name = @"remove KVO 2"; + +} + +- (void)aspectSample1 { + + QDObjectA *obj = [QDObjectA new]; + qmuiBefore(obj, @selector(setName:), NSLog(@"🎈qmui before !")) + aspectBefore(obj, @selector(setName:), NSLog(@"aspect before !")) + qmuiAfter(obj, @selector(setName:), NSLog(@"🎈qmui after !")) + aspectAfter(obj, @selector(setName:), NSLog(@"aspect after !")) + obj.name = @"aspectSample1"; +// [obj performSelector:@selector(aaa)]; +} + +- (void)sampleAspect { + + +} + +typedef NSString * (^BlockA)(int a); + +typedef struct QDStruct { + CGRect aRect; + BOOL aBool; +} QDStruct; + +- (void)func { + NSLog(@"func!"); +} + +- (id)func:(CGFloat)frame { + return nil; +} + +- (NSString *)str { + return @"xxxx"; +} + +- (void)setA:(NSObject *)a { + NSLog(@"setA - %@", a); +} + +- (NSString *)stringBy:(NSString *)aString { + return [@"stringBy" stringByAppendingString:aString]; +} + +- (void)test { + + + + [self aspect_hookSelector:@selector(stringBy:) withOptions:AspectPositionAfter usingBlock:^(id aspectInfo) { + __unsafe_unretained NSString *ori = nil; + [[aspectInfo originalInvocation] getReturnValue:&ori]; + NSLog(@""); + + } error:nil]; + + [self qmui_hookSelector:@selector(stringBy:) afterBlock:^(QMUIHookContext * _Nonnull context) { + NSLog(@"🎈 stringBy:"); + // __unsafe_unretained NSString *arg = nil; + // [context getReturnValue:&arg]; + // + // NSString *replace = @"haha"; + // [context setReturnValue:&replace]; + // [replace description]; + + }]; + + NSString *news = [self stringBy:@"aaaa"]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + // [self baseSample1]; + [self sampleKVO]; +// [self aspectSample1]; +// [self test]; + return; + + [self qmui_hookSelector:@selector(func) beforeBlock:^(QMUIHookContext * _Nonnull context) { + NSLog(@"🎈 func! before"); + }]; + + [self qmui_hookSelector:@selector(func) afterBlock:^(QMUIHookContext * _Nonnull context) { + NSLog(@"🎈 func! after"); + }]; + + [self func]; + + [self qmui_hookSelector:@selector(setBool:) afterBlock:^(QMUIHookContext * _Nonnull context) { + NSLog(@"🎈 setBool! after"); + }]; + [self setBool:YES]; + + [self qmui_hookSelector:@selector(setA:) beforeBlock:^(QMUIHookContext * _Nonnull context) { + NSString *ori = nil; +// [context getReturnValue:&ori]; +// +// NSString *aaa = nil; +// [context getArgument:&aaa atIndex:2]; +// aaa = @"ddddd"; +// [context setArgument:&aaa atIndex:2]; +// +// NSString *replace = @"zie"; +// [context setReturnValue:&replace]; +// [replace description]; + }]; + +// [self qmui_hookSelector:@selector(str) afterBlock:^(QMUIHookContext * _Nonnull context) { +// NSString *replace = @"zie"; +// [context setReturnValue:&replace]; +// [replace description]; +// }]; + + NSString *sss = [self str]; + + +// + + + + NSString *news = [self stringBy:@"aaaa"]; + + + [self setA:@"bbbc"]; + NSLog(@"%@", sss); + +} + + + +- (void)setBool:(BOOL)frame { + NSLog(@"succ!"); +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if ([keyPath isEqualToString:@"name"]) { + id newValue = [change objectForKey:NSKeyValueChangeNewKey]; + NSLog(@"🎁KVO : %@",newValue); + } + +} + +/* +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. +} +*/ + + +// +//- (void)sampleALL { +// QDObjectA *objB = [QDObjectA new]; +// +// // [objB addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL]; +// +// +// [objB aspect_hookSelector:@selector(func) withOptions:AspectPositionAfter usingBlock:^(id aspectInfo) { +// NSInvocation *originalInvocation = aspectInfo.originalInvocation; +// NSLog(@"aspect invoke!"); +// +// } error:nil]; +// +// [objB func]; +// +// +// [objB qmui_hookSelector:@selector(setName:) afterBlock:^{ +// +// }]; +// +// +// +// // [objB removeObserver:self forKeyPath:@"name"]; +// +// +// +// +// objB.name = @"111"; +// +// QDObjectA *objB2 = [QDObjectA new]; +// objB2.name = @"222"; +// +//// [objB performSelector:@selector(sel)]; +// +//// [objB func]; +//} + +@end diff --git a/qmuidemo/Modules/Demos/About/QDAAViewController.h b/qmuidemo/Modules/Demos/About/QDAAViewController.h deleted file mode 100644 index d68c35ae..00000000 --- a/qmuidemo/Modules/Demos/About/QDAAViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDAAViewController.h -// qmuidemo -// -// Created by QMUI Team on 2018/7/12. -// Copyright © 2018年 QMUI Team. All rights reserved. -// - -#import "QDCommonViewController.h" - -@interface QDAAViewController : QMUIDialogTextFieldViewController - -@end diff --git a/qmuidemo/Modules/Demos/About/QDAAViewController.m b/qmuidemo/Modules/Demos/About/QDAAViewController.m deleted file mode 100644 index 9e5880af..00000000 --- a/qmuidemo/Modules/Demos/About/QDAAViewController.m +++ /dev/null @@ -1,109 +0,0 @@ -// -// QDAAViewController.m -// qmuidemo -// -// Created by QMUI Team on 2018/7/12. -// Copyright © 2018年 QMUI Team. All rights reserved. -// - -#import "QDAAViewController.h" - -@interface QDAAViewController () - -@property(nonatomic, copy) void (^textFieldConfigurationHandler)(QMUILabel *titleLabel, QMUITextField *textField, CALayer *separatorLayer); -@property(nonatomic, strong) QMUIFillButton *addPeopleButton; - -/// 优惠金额 -@property(nonatomic, weak) QMUITextField *discountPayField; - -/// 实付金额 -@property(nonatomic, weak) QMUITextField *actuallyPayField; - -/// 其中运费 -@property(nonatomic, weak) QMUITextField *freightPayField; -@end - -@implementation QDAAViewController - -- (void)didInitialize { - [super didInitialize]; - self.title = @"拼单计算器"; - self.textFieldMargins = UIEdgeInsetsMake(8, 16, 6, 16); - self.textFieldSeparatorInsets = UIEdgeInsetsMake(0, 0, 8, 0); -} - -- (void)viewDidLoad { - [super viewDidLoad]; - - __weak __typeof(self)weakSelf = self; - self.textFieldConfigurationHandler = ^(QMUILabel *titleLabel, QMUITextField *textField, CALayer *separatorLayer) { - titleLabel.font = UIFontMake(12); - titleLabel.textColor = UIColorGray1; - textField.placeholder = @"输入原价"; - textField.font = UIFontMake(12); - textField.keyboardType = UIKeyboardTypeDecimalPad; - }; - - [self addTextFieldWithTitle:@"团友1" configurationHandler:self.textFieldConfigurationHandler]; - [self addTextFieldWithTitle:@"团友2" configurationHandler:self.textFieldConfigurationHandler]; - [self addTextFieldWithTitle:@"团友3" configurationHandler:self.textFieldConfigurationHandler]; - [self addTextFieldWithTitle:@"团友4" configurationHandler:self.textFieldConfigurationHandler]; - [self addTextFieldWithTitle:@"团友5" configurationHandler:self.textFieldConfigurationHandler]; - [self addTextFieldWithTitle:@"优惠金额" configurationHandler:^(QMUILabel *titleLabel, QMUITextField *textField, CALayer *separatorLayer) { - weakSelf.textFieldConfigurationHandler(titleLabel, textField, separatorLayer); - textField.placeholder = @"减免的那部分"; - }]; - [self addTextFieldWithTitle:@"实付金额" configurationHandler:^(QMUILabel *titleLabel, QMUITextField *textField, CALayer *separatorLayer) { - weakSelf.textFieldConfigurationHandler(titleLabel, textField, separatorLayer); - textField.placeholder = @"最终全单实际支付金额"; - }]; - [self addTextFieldWithTitle:@"其中运费" configurationHandler:^(QMUILabel *titleLabel, QMUITextField *textField, CALayer *separatorLayer) { - weakSelf.textFieldConfigurationHandler(titleLabel, textField, separatorLayer); - textField.placeholder = @"运费将按每人消费比例分配"; - }]; - [self addCancelButtonWithText:@"关闭" block:^(QMUIDialogTextFieldViewController *aDialogViewController) { - [aDialogViewController hide]; - }]; - [self addSubmitButtonWithText:@"计算" block:^(QMUIDialogTextFieldViewController *aDialogViewController) { - [weakSelf calculatePrice]; - }]; -} - -- (void)calculatePrice { - NSArray *textFields = self.textFields; - double total = textFields[textFields.count - 2].text.doubleValue; - double discountMoney = textFields[textFields.count - 3].text.doubleValue; - double freight = textFields.lastObject.text.doubleValue; - double discount = (total - freight) / (total - freight + discountMoney);// 除运费外的折扣比例 - - double (^aaMoneyCounter)(double aaOriginalPrice) = ^double(double aaOriginalPrice) { - return aaOriginalPrice * discount + freight * (aaOriginalPrice / (total + discountMoney - freight)); - }; - - NSMutableArray *aaMoney = [[NSMutableArray alloc] init]; - NSMutableString *aaMoneyString = [[NSMutableString alloc] initWithString:@"拼单账单"]; - double checkResult = 0; - for (NSInteger i = 0; i < textFields.count - 3; i++) { - double result = aaMoneyCounter(textFields[i].text.doubleValue); - checkResult += result; - [aaMoney addObject:@(result)]; - [aaMoneyString appendFormat:@"\n团友%@: %.1f", @(i), result]; - } - NSAssert(fabs(checkResult - total) < 1, @"每个人的拼单金额加起来与实付金额差得有点多,实付金额:%.1f,拼单结果总额:%.1f", total, checkResult); - [QMUITips showWithText:aaMoneyString.copy inView:self.view hideAfterDelay:0]; -} - -- (void)viewDidAppear:(BOOL)animated { - [super viewDidAppear:animated]; - NSArray *textFields = self.textFields; - textFields[0].text = @"17.9"; - textFields[1].text = @"16.8"; - textFields[2].text = @"29.9"; - textFields[3].text = @"49.9"; - textFields[4].text = @"71.6"; - textFields[5].text = @"20"; - textFields[6].text = @"166.6"; - textFields[7].text = @"0"; -} - -@end diff --git a/qmuidemo/Modules/Demos/About/QDAboutViewController.m b/qmuidemo/Modules/Demos/About/QDAboutViewController.m index e35656bf..edc8b3c8 100644 --- a/qmuidemo/Modules/Demos/About/QDAboutViewController.m +++ b/qmuidemo/Modules/Demos/About/QDAboutViewController.m @@ -7,86 +7,39 @@ // #import "QDAboutViewController.h" -#import "QDAAViewController.h" @interface QDAboutViewController () -@property(nonatomic, strong) UIImage *themeAboutLogoImage; @property(nonatomic, strong) UIScrollView *scrollView; @property(nonatomic, strong) UIImageView *logoImageView; @property(nonatomic, strong) QMUIButton *versionButton; -@property(nonatomic, strong) QMUIButton *websiteButton; -@property(nonatomic, strong) QMUIButton *documentButton; @property(nonatomic, strong) QMUIButton *gitHubButton; @property(nonatomic, strong) UILabel *copyrightLabel; -@property(nonatomic, assign) NSInteger tapCount; @end @implementation QDAboutViewController -- (void)didInitialize { - [super didInitialize]; - - NSString *imagePath = [[NSUserDefaults standardUserDefaults] objectForKey:[self userDefaultsKeyForAboutLogoImage]]; - if (imagePath) { - UIImage *aboutLogoImage = [UIImage imageWithContentsOfFile:imagePath]; - if (aboutLogoImage) { - self.themeAboutLogoImage = aboutLogoImage; - return; - } - } - - // 如果是第一次进来,则在 viewDidAppear 之后再让 logo 图渐变显示出来 - __weak __typeof(self)weakSelf = self; - self.qmui_didAppearAndLoadDataBlock = ^{ - __strong __typeof(weakSelf)strongSelf = weakSelf; - [strongSelf transitLogoImageIfNeeded]; - }; - - dispatch_async(dispatch_get_global_queue(0, 0), ^{ - UIImage *aboutLogoImage = UIImageMake(@"about_logo_monochrome"); - UIImage *blendedAboutLogoImage = [aboutLogoImage qmui_imageWithBlendColor:[QDThemeManager sharedInstance].currentTheme.themeTintColor]; - [self saveImageAsFile:blendedAboutLogoImage]; - dispatch_async(dispatch_get_main_queue(), ^{ - self.themeAboutLogoImage = blendedAboutLogoImage; - self.qmui_dataLoaded = YES; - }); - }); -} - - (void)initSubviews { [super initSubviews]; self.scrollView = [[UIScrollView alloc] init]; [self.view addSubview:self.scrollView]; - self.logoImageView = [[UIImageView alloc] initWithImage:self.themeAboutLogoImage ?: UIImageMake(@"about_logo_monochrome")]; + self.logoImageView = [[UIImageView alloc] initWithImage:[UIImageMake(@"launch_logo") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]]; + self.logoImageView.tintColor = UIColorGray9; self.logoImageView.userInteractionEnabled = YES; [self.scrollView addSubview:self.logoImageView]; - UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; - tap.numberOfTapsRequired = 3; - [self.logoImageView addGestureRecognizer:tap]; NSString *appVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; self.versionButton = [[QMUIButton alloc] init]; self.versionButton.titleLabel.font = UIFontMake(14); - [self.versionButton setTitleColor:UIColorGray3 forState:UIControlStateNormal]; + [self.versionButton setTitleColor:UIColor.qd_mainTextColor forState:UIControlStateNormal]; [self.versionButton setTitle:[NSString stringWithFormat:@"版本 %@", appVersion] forState:UIControlStateNormal]; [self.versionButton sizeToFit]; self.versionButton.qmui_outsideEdge = UIEdgeInsetsMake(-12, -12, -12, -12); [self.versionButton addTarget:self action:@selector(handleVersionButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; [self.scrollView addSubview:self.versionButton]; - self.websiteButton = [self generateCellButtonWithTitle:@"访问官网"]; - self.websiteButton.qmui_borderPosition = QMUIViewBorderPositionTop; - [self.websiteButton addTarget:self action:@selector(handleWebsiteButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self.scrollView addSubview:self.websiteButton]; - - self.documentButton = [self generateCellButtonWithTitle:@"功能列表"]; - self.documentButton.qmui_borderPosition = QMUIViewBorderPositionTop; - [self.documentButton addTarget:self action:@selector(handleDocumentButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self.scrollView addSubview:self.documentButton]; - self.gitHubButton = [self generateCellButtonWithTitle:@"GitHub"]; self.gitHubButton.qmui_borderPosition = QMUIViewBorderPositionTop | QMUIViewBorderPositionBottom; [self.gitHubButton addTarget:self action:@selector(handleGitHubButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; @@ -94,7 +47,7 @@ - (void)initSubviews { self.copyrightLabel = [[UILabel alloc] init]; self.copyrightLabel.numberOfLines = 0; - self.copyrightLabel.attributedText = [[NSAttributedString alloc] initWithString:@"© 2019 QMUI Team All Rights Reserved." attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColorGray5, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:16 lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentCenter]}]; + self.copyrightLabel.attributedText = [[NSAttributedString alloc] initWithString:@"© 2024 QMUI Team All Rights Reserved." attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:16 lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentCenter]}]; [self.scrollView addSubview:self.copyrightLabel]; } @@ -103,19 +56,11 @@ - (void)setupNavigationItems { self.title = @"关于"; } -- (void)transitLogoImageIfNeeded { - if (self.themeAboutLogoImage && self.logoImageView && self.logoImageView.image != self.themeAboutLogoImage) { - UIImageView *templateImageView = [[UIImageView alloc] initWithFrame:self.logoImageView.bounds]; - templateImageView.image = self.themeAboutLogoImage; - templateImageView.alpha = 0; - [self.logoImageView addSubview:templateImageView]; - [UIView animateWithDuration:1.0 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ - templateImageView.alpha = 1; - } completion:^(BOOL finished) { - self.logoImageView.image = self.themeAboutLogoImage; - [templateImageView removeFromSuperview]; - }]; - } +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [UIView animateWithDuration:1.0 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + self.logoImageView.tintColor = UIColor.qd_tintColor; + } completion:nil]; } - (QMUIButton *)generateCellButtonWithTitle:(NSString *)title { @@ -138,7 +83,7 @@ - (void)viewDidLayoutSubviews { CGFloat buttonHeight = TableViewCellNormalHeight; UIEdgeInsets padding = UIEdgeInsetsMake(24, 24, 24, 24); - if (self.view.qmui_safeAreaInsets.bottom > 0) { + if (self.view.safeAreaInsets.bottom > 0) { padding.bottom = padding.bottom - 20; } @@ -154,17 +99,15 @@ - (void)viewDidLayoutSubviews { self.versionButton.frame = CGRectSetXY(self.versionButton.frame, CGRectGetMinXHorizontallyCenter(self.logoImageView.frame, self.versionButton.frame), CGRectGetMaxY(self.logoImageView.frame) + versionLabelMarginTop); CGFloat contentWidthInRight = rightWidth - UIEdgeInsetsGetHorizontalValue(padding); - self.websiteButton.frame = CGRectMake(leftWidth + padding.left, CGRectGetMinY(self.logoImageView.frame) + 10, contentWidthInRight, buttonHeight); - self.documentButton.frame = CGRectSetY(self.websiteButton.frame, CGRectGetMaxY(self.websiteButton.frame)); - self.gitHubButton.frame = CGRectSetY(self.websiteButton.frame, CGRectGetMaxY(self.documentButton.frame)); + self.gitHubButton.frame = CGRectMake(leftWidth + padding.left, CGRectGetMinY(self.logoImageView.frame) + 10, contentWidthInRight, buttonHeight); CGFloat copyrightLabelHeight = [self.copyrightLabel sizeThatFits:CGSizeMake(contentWidthInRight, CGFLOAT_MAX)].height; - self.copyrightLabel.frame = CGRectFlatMake(leftWidth + padding.left, CGRectGetHeight(self.scrollView.bounds) - UIEdgeInsetsGetVerticalValue(self.scrollView.qmui_contentInset) - padding.bottom - copyrightLabelHeight, contentWidthInRight, copyrightLabelHeight); + self.copyrightLabel.frame = CGRectFlatMake(leftWidth + padding.left, CGRectGetHeight(self.scrollView.bounds) - UIEdgeInsetsGetVerticalValue(self.scrollView.adjustedContentInset) - padding.bottom - copyrightLabelHeight, contentWidthInRight, copyrightLabelHeight); - self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds) - UIEdgeInsetsGetVerticalValue(self.scrollView.qmui_contentInset)); + self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds) - UIEdgeInsetsGetVerticalValue(self.scrollView.adjustedContentInset)); } else { - CGFloat containerHeight = CGRectGetHeight(self.scrollView.bounds) - UIEdgeInsetsGetVerticalValue(self.scrollView.qmui_contentInset) - UIEdgeInsetsGetVerticalValue(padding); + CGFloat containerHeight = CGRectGetHeight(self.scrollView.bounds) - UIEdgeInsetsGetVerticalValue(self.scrollView.adjustedContentInset) - UIEdgeInsetsGetVerticalValue(padding); CGFloat buttonMarginTop = 36; CGFloat mainContentHeight = CGRectGetHeight(self.logoImageView.frame) + versionLabelMarginTop + CGRectGetHeight(self.versionButton.frame) + buttonMarginTop + buttonHeight * 2; CGFloat mainContentMinY = padding.top + (containerHeight - mainContentHeight) / 6; @@ -178,63 +121,28 @@ - (void)viewDidLayoutSubviews { self.versionButton.frame = CGRectSetXY(self.versionButton.frame, CGRectGetMinXHorizontallyCenterInParentRect(self.scrollView.bounds, self.versionButton.frame), CGRectGetMaxY(self.logoImageView.frame) + versionLabelMarginTop); - self.websiteButton.frame = CGRectMake(0, CGRectGetMaxY(self.versionButton.frame) + buttonMarginTop, CGRectGetWidth(self.scrollView.bounds), buttonHeight); - self.documentButton.frame = CGRectSetY(self.websiteButton.frame, CGRectGetMaxY(self.websiteButton.frame)); - self.gitHubButton.frame = CGRectSetY(self.documentButton.frame, CGRectGetMaxY(self.documentButton.frame)); + self.gitHubButton.frame = CGRectMake(0, CGRectGetMaxY(self.versionButton.frame) + buttonMarginTop, CGRectGetWidth(self.scrollView.bounds), buttonHeight); CGFloat copyrightLabelWidth = CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(padding); CGFloat copyrightLabelHeight = [self.copyrightLabel sizeThatFits:CGSizeMake(copyrightLabelWidth, CGFLOAT_MAX)].height; - self.copyrightLabel.frame = CGRectFlatMake(padding.left, CGRectGetHeight(self.scrollView.bounds) - UIEdgeInsetsGetVerticalValue(self.scrollView.qmui_contentInset) - padding.bottom - copyrightLabelHeight, copyrightLabelWidth, copyrightLabelHeight); + self.copyrightLabel.frame = CGRectFlatMake(padding.left, CGRectGetHeight(self.scrollView.bounds) - UIEdgeInsetsGetVerticalValue(self.scrollView.adjustedContentInset) - padding.bottom - copyrightLabelHeight, copyrightLabelWidth, copyrightLabelHeight); self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.scrollView.bounds), CGRectGetMaxY(self.copyrightLabel.frame) + padding.bottom); } } -- (NSString *)userDefaultsKeyForAboutLogoImage { - return [NSString stringWithFormat:@"about_logo_%@@%.0fx.png", [QDThemeManager sharedInstance].currentTheme.themeName, ScreenScale]; -} - -- (void)saveImageAsFile:(UIImage *)image { - NSData *imageData = UIImagePNGRepresentation(image); - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSString *documentsDirectory = paths.firstObject; - NSString *imageName = [self userDefaultsKeyForAboutLogoImage]; - NSString *imagePath = [documentsDirectory stringByAppendingPathComponent:imageName]; - - if ([imageData writeToFile:imagePath atomically:NO]) { - [[NSUserDefaults standardUserDefaults] setObject:imagePath forKey:imageName]; - } -} - - (void)handleVersionButtonEvent:(QMUIButton *)button { [self openUrlString:@"https://github.com/Tencent/QMUI_iOS/releases"]; } -- (void)handleWebsiteButtonEvent:(QMUIButton *)button { - [self openUrlString:@"https://qmuiteam.com/ios"]; -} - -- (void)handleDocumentButtonEvent:(QMUIButton *)button { - [self openUrlString:@"https://qmuiteam.com/ios/page/document.html"]; -} - - (void)handleGitHubButtonEvent:(QMUIButton *)button { [self openUrlString:@"https://github.com/QMUI/QMUI_iOS"]; } - (void)openUrlString:(NSString *)urlString { - UIApplication *application = [UIApplication sharedApplication]; + UIApplication *application = UIApplication.sharedApplication; NSURL *url = [NSURL URLWithString:urlString]; - if (@available(iOS 10, *)) { - [application openURL:url options:@{} completionHandler:nil]; - } else { - [application openURL:url]; - } -} - -- (void)handleTapGesture:(UITapGestureRecognizer *)gesture { - QDAAViewController *viewController = [[QDAAViewController alloc] init]; - [viewController show]; + [application openURL:url options:@{} completionHandler:nil]; } @end diff --git a/qmuidemo/Modules/Demos/Components/QDBadgeViewController.h b/qmuidemo/Modules/Demos/Components/QDBadgeViewController.h index 8e873743..7ecf4944 100644 --- a/qmuidemo/Modules/Demos/Components/QDBadgeViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDBadgeViewController.h @@ -6,8 +6,8 @@ // Copyright © 2018年 QMUI Team. All rights reserved. // -#import "QDCommonListViewController.h" +#import "QDCommonGroupListViewController.h" -@interface QDBadgeViewController : QDCommonListViewController +@interface QDBadgeViewController : QDCommonGroupListViewController @end diff --git a/qmuidemo/Modules/Demos/Components/QDBadgeViewController.m b/qmuidemo/Modules/Demos/Components/QDBadgeViewController.m index 183eb289..f073045d 100644 --- a/qmuidemo/Modules/Demos/Components/QDBadgeViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDBadgeViewController.m @@ -10,6 +10,7 @@ @interface QDBadgeViewController () +@property(nonatomic, strong) UIToolbar *toolbar; @property(nonatomic, strong) UITabBar *tabBar; @end @@ -17,13 +18,34 @@ @implementation QDBadgeViewController - (void)setupNavigationItems { [super setupNavigationItems]; - self.navigationItem.rightBarButtonItem = [UIBarButtonItem qmui_itemWithImage:UIImageMake(@"icon_nav_about") target:nil action:NULL]; + self.navigationItem.rightBarButtonItems = @[ + [UIBarButtonItem qmui_itemWithTitle:@"文字" target:nil action:NULL], + [UIBarButtonItem qmui_itemWithImage:UIImageMake(@"icon_nav_about") target:nil action:NULL], + [UIBarButtonItem qmui_itemWithButton:[[QMUINavigationButton alloc] initWithType:QMUINavigationButtonTypeNormal title:@"自定义"] target:nil action:NULL], + ]; } - (void)initSubviews { [super initSubviews]; + + self.toolbar = [[UIToolbar alloc] init]; + self.toolbar.tintColor = UIColor.qd_tintColor; + self.toolbar.items = @[ + [UIBarButtonItem qmui_flexibleSpaceItem], + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay target:nil action:NULL], + [UIBarButtonItem qmui_flexibleSpaceItem], + [UIBarButtonItem qmui_itemWithImage:UIImageMake(@"icon_tabbar_uikit") target:nil action:NULL], + [UIBarButtonItem qmui_flexibleSpaceItem], + [UIBarButtonItem qmui_itemWithTitle:@"ToolbarItem" target:nil action:NULL], + [UIBarButtonItem qmui_flexibleSpaceItem], + ]; + [self.toolbar sizeToFit]; + self.toolbar.items[1].qmui_shouldShowUpdatesIndicator = YES; + self.toolbar.items[3].qmui_badgeInteger = 8; + self.toolbar.items[5].qmui_badgeString = @"99+"; + [self.view addSubview:self.toolbar]; + self.tabBar = [[UITabBar alloc] init]; - self.tabBar.tintColor = TabBarTintColor; UITabBarItem *item1 = [QDUIHelper tabBarItemWithTitle:@"QMUIKit" image:[UIImageMake(@"icon_tabbar_uikit") imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] selectedImage:UIImageMake(@"icon_tabbar_uikit_selected") tag:0]; @@ -36,33 +58,46 @@ - (void)initSubviews { self.tabBar.selectedItem = item1; [self.tabBar sizeToFit]; [self.view addSubview:self.tabBar]; - - // 统一调整横屏模式下 UITabBarItem 的红点和未读数偏移位置(具体值视业务设计不同而不同) - [self.tabBar.items enumerateObjectsUsingBlock:^(UITabBarItem * _Nonnull item, NSUInteger idx, BOOL * _Nonnull stop) { - item.qmui_badgeCenterOffsetLandscape = CGPointMake(9, -7); - }]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + cell.textLabel.qmui_shouldShowUpdatesIndicator = YES; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; CGFloat tabBarHeight = TabBarHeight; self.tabBar.frame = CGRectMake(0, CGRectGetHeight(self.view.bounds) - tabBarHeight, CGRectGetWidth(self.view.bounds), tabBarHeight); + self.toolbar.frame = CGRectMake(0, CGRectGetMinY(self.tabBar.frame) - CGRectGetHeight(self.toolbar.frame), CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.toolbar.frame)); } - (void)initDataSource { - self.dataSource = @[@"在 UIBarButtonItem 上显示红点", - @"在 UITabBarItem 上显示红点", - @"在 UITabBarItem 上显示未读数", - @"修改红点/未读数的样式(多点几次试试)", - @"隐藏所有红点、未读数"]; + self.dataSource = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"UIView", [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"在 UIView 上显示红点", @"点击可切换红点的显隐", + nil], + @"UIBarItem", [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"在 UIBarButtonItem 上显示红点", @"", + @"在 UITabBarItem 上显示红点", @"", + @"在 UITabBarItem 上显示未读数", @"", + @"修改红点/未读数的样式(多点几次试试)", @"", + @"隐藏所有红点、未读数", @"", + nil], + nil]; } - (void)didSelectCellWithTitle:(NSString *)title { - if ([title isEqualToString:@"在 UIBarButtonItem 上显示红点"]) { + if ([title isEqualToString:@"在 UIView 上显示红点"]) { + UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + cell.textLabel.qmui_shouldShowUpdatesIndicator = !cell.textLabel.qmui_shouldShowUpdatesIndicator; + } else if ([title isEqualToString:@"在 UIBarButtonItem 上显示红点"]) { // 有使用配置表的时候,最简单的代码就只是控制显隐即可,没使用配置表的话,还需要设置其他的属性才能使红点样式正确,具体请看 UIBarButton+QMUIBadge.h 注释 - self.navigationItem.rightBarButtonItem.qmui_shouldShowUpdatesIndicator = YES; - + self.navigationItem.rightBarButtonItems[0].qmui_badgeString = @"8"; + self.navigationItem.rightBarButtonItems[1].qmui_badgeString = @"9"; + self.navigationItem.rightBarButtonItems[2].qmui_shouldShowUpdatesIndicator = YES; } else if ([title isEqualToString:@"在 UITabBarItem 上显示红点"]) { UITabBarItem *item = self.tabBar.items.firstObject; @@ -83,7 +118,16 @@ - (void)didSelectCellWithTitle:(NSString *)title { } else if ([title isEqualToString:@"隐藏所有红点、未读数"]) { - self.navigationItem.rightBarButtonItem.qmui_shouldShowUpdatesIndicator = NO; + UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + cell.textLabel.qmui_shouldShowUpdatesIndicator = NO; + [self.navigationItem.rightBarButtonItems enumerateObjectsUsingBlock:^(UIBarButtonItem * _Nonnull item, NSUInteger idx, BOOL * _Nonnull stop) { + item.qmui_shouldShowUpdatesIndicator = NO; + item.qmui_badgeInteger = 0; + }]; + [self.toolbar.items enumerateObjectsUsingBlock:^(UIBarButtonItem * _Nonnull item, NSUInteger idx, BOOL * _Nonnull stop) { + item.qmui_shouldShowUpdatesIndicator = NO; + item.qmui_badgeInteger = 0; + }]; [self.tabBar.items enumerateObjectsUsingBlock:^(UITabBarItem * _Nonnull item, NSUInteger idx, BOOL * _Nonnull stop) { item.qmui_shouldShowUpdatesIndicator = NO; item.qmui_badgeInteger = 0; diff --git a/qmuidemo/Modules/Demos/Components/QDCellKeyCacheViewController.h b/qmuidemo/Modules/Demos/Components/QDCellKeyCacheViewController.h deleted file mode 100644 index eda0ed1e..00000000 --- a/qmuidemo/Modules/Demos/Components/QDCellKeyCacheViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDCellKeyCacheViewController.h -// qmuidemo -// -// Created by QMUI Team on 2018/3/18. -// Copyright © 2018年 QMUI Team. All rights reserved. -// - -#import "QDCommonListViewController.h" - -@interface QDCellKeyCacheViewController : QDCommonListViewController - -@end diff --git a/qmuidemo/Modules/Demos/Components/QDCellKeyCacheViewController.m b/qmuidemo/Modules/Demos/Components/QDCellKeyCacheViewController.m deleted file mode 100644 index af419263..00000000 --- a/qmuidemo/Modules/Demos/Components/QDCellKeyCacheViewController.m +++ /dev/null @@ -1,39 +0,0 @@ -// -// QDCellKeyCacheViewController.m -// qmuidemo -// -// Created by QMUI Team on 2018/3/18. -// Copyright © 2018年 QMUI Team. All rights reserved. -// - -#import "QDCellKeyCacheViewController.h" -#import "QDCellHeightCacheViewController.h" -#import "QDCellHeightKeyCacheViewController.h" -#import "QDCellSizeKeyCacheViewController.h" - -@interface QDCellKeyCacheViewController () - -@end - -@implementation QDCellKeyCacheViewController - -- (void)initDataSource { - self.dataSource = @[@"QMUICellHeightCache", - @"QMUICellHeightKeyCache(estimated)", - @"QMUICellSizeKeyCache(暂不能使用)"]; -} - -- (void)didSelectCellWithTitle:(NSString *)title { - UIViewController *viewController = nil; - if ([title isEqualToString:@"QMUICellHeightCache"]) { - viewController = [[QDCellHeightCacheViewController alloc] init]; - } else if ([title isEqualToString:@"QMUICellHeightKeyCache(estimated)"]) { - viewController = [[QDCellHeightKeyCacheViewController alloc] init]; - } else if ([title isEqualToString:@"QMUICellSizeKeyCache(暂不能使用)"]) { - viewController = [[QDCellSizeKeyCacheViewController alloc] init]; - } - viewController.title = title; - [self.navigationController pushViewController:viewController animated:YES]; -} - -@end diff --git a/qmuidemo/Modules/Demos/Components/QDCellSizeKeyCacheViewController.m b/qmuidemo/Modules/Demos/Components/QDCellSizeKeyCacheViewController.m index 55a61ea7..25b41d0a 100644 --- a/qmuidemo/Modules/Demos/Components/QDCellSizeKeyCacheViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDCellSizeKeyCacheViewController.m @@ -64,7 +64,7 @@ - (void)initSubviews { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; self.collectionView.frame = self.view.bounds; - self.collectionLayout.sectionInset = UIEdgeInsetsMake(24, 24 + self.view.qmui_safeAreaInsets.left, 24, 24 + self.view.qmui_safeAreaInsets.right);; + self.collectionLayout.sectionInset = UIEdgeInsetsMake(24, 24 + self.view.safeAreaInsets.left, 24, 24 + self.view.safeAreaInsets.right);; self.collectionLayout.estimatedItemSize = CGSizeMake(CGRectGetWidth(self.collectionView.bounds) - UIEdgeInsetsGetHorizontalValue(self.collectionLayout.sectionInset), 300); } @@ -101,10 +101,7 @@ - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.backgroundColor = UIColorWhite; - self.layer.shadowColor = UIColorBlack.CGColor; - self.layer.shadowOpacity = .1; - self.layer.shadowRadius = 15; - self.layer.shadowOffset = CGSizeMake(0, 1); + self.layer.qmui_shadow = [NSShadow qmui_shadowWithColor:[UIColorBlack colorWithAlphaComponent:.1] shadowOffset:CGSizeMake(0, 1) shadowRadius:15]; self.layer.cornerRadius = 6; self.textLabel = [[UILabel alloc] init]; diff --git a/qmuidemo/Modules/Demos/Components/QDCheckboxViewController.h b/qmuidemo/Modules/Demos/Components/QDCheckboxViewController.h new file mode 100644 index 00000000..36dd1c99 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDCheckboxViewController.h @@ -0,0 +1,17 @@ +// +// QDCheckboxViewController.h +// qmuidemo +// +// Created by molice on 2024/8/1. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDCheckboxViewController : QDCommonViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Components/QDCheckboxViewController.m b/qmuidemo/Modules/Demos/Components/QDCheckboxViewController.m new file mode 100644 index 00000000..f3065835 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDCheckboxViewController.m @@ -0,0 +1,99 @@ +// +// QDCheckboxViewController.m +// qmuidemo +// +// Created by molice on 2024/8/1. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import "QDCheckboxViewController.h" +#import "QMUIInteractiveDebugger.h" + +@interface QDCheckboxViewController () +@property(nonatomic, strong) QMUICheckbox *checkbox; +@property(nonatomic, strong) QMUIInteractiveDebugPanelViewController *debugViewController; +@end + +@implementation QDCheckboxViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.checkbox = QMUICheckbox.new; + self.checkbox.spacingBetweenImageAndTitle = 8; + self.checkbox.titleLabel.font = UIFontMake(16); + self.checkbox.adjustsTitleTintColorAutomatically = YES; + [self.checkbox addTarget:self action:@selector(handleCheckboxEvent) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.checkbox]; + + __weak __typeof(self)weakSelf = self; + __weak __typeof(self.checkbox)weakCheckbox = self.checkbox; + self.debugViewController = [QDUIHelper generateDebugViewControllerWithTitle:@"配置参数" items:@[ + [QMUIInteractiveDebugPanelItem enumItemWithTitle:@"状态" items:@[@"Normal", @"Selected", @"Indeterminate", @"Disabled"] valueGetter:^(QMUIButton * _Nonnull actionView, NSArray * _Nonnull items) { + NSInteger index = 0; + if (weakCheckbox.state == UIControlStateNormal) index = 0; + else if (weakCheckbox.state == UIControlStateSelected) index = 1; + else if (weakCheckbox.indeterminate) index = 2; + else if (!weakCheckbox.enabled) index = 3; + [actionView setTitle:items[index] forState:UIControlStateNormal]; + } valueSetter:^(QMUIButton * _Nonnull actionView, NSArray * _Nonnull items) { + NSInteger index = [items indexOfObject:actionView.currentTitle]; + switch (index) { + case 0: + weakCheckbox.enabled = YES; + weakCheckbox.selected = NO; + weakCheckbox.indeterminate = NO; + break; + case 1: + weakCheckbox.enabled = YES; + weakCheckbox.selected = YES; + break; + case 2: + weakCheckbox.enabled = YES; + weakCheckbox.indeterminate = YES; + break; + case 3: + weakCheckbox.enabled = NO; + break; + default: + break; + } + }], + [QMUIInteractiveDebugPanelItem sliderItemWithTitle:@"尺寸" minValue:12 maxValue:40 valueGetter:^(UISlider * _Nonnull actionView) { + actionView.value = weakCheckbox.checkboxSize.width; + } valueSetter:^(UISlider * _Nonnull actionView) { + weakCheckbox.checkboxSize = CGSizeMake(actionView.value, actionView.value); + [weakSelf.view setNeedsLayout]; + }], + [QMUIInteractiveDebugPanelItem colorItemWithTitle:@"颜色" valueGetter:^(QMUITextField * _Nonnull actionView) { + actionView.text = weakCheckbox.tintColor.qmui_RGBAString; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + weakCheckbox.tintColor = [UIColor qmui_colorWithRGBAString:actionView.text]; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"文本" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakCheckbox.currentTitle.length > 0; + } valueSetter:^(UISwitch * _Nonnull actionView) { + [weakCheckbox setTitle:actionView.on ? @"同意用户及隐私协议" : nil forState:UIControlStateNormal]; + [weakSelf.view setNeedsLayout]; + }], + ]]; + [self.view addSubview:self.debugViewController.view]; +} + +- (void)handleCheckboxEvent { + if (!self.checkbox.selected && !self.checkbox.indeterminate) self.checkbox.selected = YES; + else if (self.checkbox.selected) self.checkbox.indeterminate = YES; + else { + self.checkbox.selected = NO; + self.checkbox.indeterminate = NO; + } +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + [self.checkbox sizeToFit]; + self.checkbox.center = CGPointMake(CGRectGetWidth(self.view.bounds) / 2, self.qmui_navigationBarMaxYInViewCoordinator + 32 + self.checkbox.qmui_height / 2); + CGSize size = [self.debugViewController contentSizeThatFits:CGSizeMake(320, CGFLOAT_MAX)]; + self.debugViewController.view.frame = CGRectMake(CGFloatGetCenter(CGRectGetWidth(self.view.bounds), 320), 200, 320, size.height); +} + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDCollectionDemoViewController.m b/qmuidemo/Modules/Demos/Components/QDCollectionDemoViewController.m index adc30b7a..5426eecf 100644 --- a/qmuidemo/Modules/Demos/Components/QDCollectionDemoViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDCollectionDemoViewController.m @@ -82,9 +82,9 @@ - (void)handleDebugItemEvent { - (UIEdgeInsets)sectionInset { if (self.collectionViewLayout.debug) { CGSize itemSize = CGSizeMake(100, 100); - CGFloat horizontalInset = (CGRectGetWidth(self.collectionView.bounds) - UIEdgeInsetsGetHorizontalValue(self.collectionView.qmui_contentInset) - itemSize.width) / 2; - CGFloat verticalInset = (CGRectGetHeight(self.collectionView.bounds) - UIEdgeInsetsGetVerticalValue(self.collectionView.qmui_contentInset) - itemSize.height) / 2; - return UIEdgeInsetsMake(verticalInset, horizontalInset, verticalInset, CGRectGetWidth(self.collectionView.bounds) - horizontalInset - itemSize.width - UIEdgeInsetsGetHorizontalValue(self.collectionView.qmui_contentInset)); + CGFloat horizontalInset = (CGRectGetWidth(self.collectionView.bounds) - UIEdgeInsetsGetHorizontalValue(self.collectionView.adjustedContentInset) - itemSize.width) / 2; + CGFloat verticalInset = (CGRectGetHeight(self.collectionView.bounds) - UIEdgeInsetsGetVerticalValue(self.collectionView.adjustedContentInset) - itemSize.height) / 2; + return UIEdgeInsetsMake(verticalInset, horizontalInset, verticalInset, CGRectGetWidth(self.collectionView.bounds) - horizontalInset - itemSize.width - UIEdgeInsetsGetHorizontalValue(self.collectionView.adjustedContentInset)); } else { return UIEdgeInsetsMake(36, 36, 36, 36); } @@ -118,7 +118,7 @@ - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollection return CGSizeMake(100, 100); } - CGSize size = CGSizeMake(CGRectGetWidth(collectionView.bounds) - UIEdgeInsetsGetHorizontalValue(self.collectionViewLayout.sectionInset) - UIEdgeInsetsGetHorizontalValue(self.collectionView.qmui_contentInset), CGRectGetHeight(collectionView.bounds) - UIEdgeInsetsGetVerticalValue(self.collectionViewLayout.sectionInset) - UIEdgeInsetsGetVerticalValue(self.collectionView.qmui_contentInset)); + CGSize size = CGSizeMake(CGRectGetWidth(collectionView.bounds) - UIEdgeInsetsGetHorizontalValue(self.collectionViewLayout.sectionInset) - UIEdgeInsetsGetHorizontalValue(self.collectionView.adjustedContentInset), CGRectGetHeight(collectionView.bounds) - UIEdgeInsetsGetVerticalValue(self.collectionViewLayout.sectionInset) - UIEdgeInsetsGetVerticalValue(self.collectionView.adjustedContentInset)); return size; } diff --git a/qmuidemo/Modules/Demos/Components/QDCollectionListViewController.h b/qmuidemo/Modules/Demos/Components/QDCollectionListViewController.h deleted file mode 100644 index f740e83a..00000000 --- a/qmuidemo/Modules/Demos/Components/QDCollectionListViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDCollectionListViewController.h -// qmuidemo -// -// Created by QMUI Team on 15/9/24. -// Copyright © 2015年 QMUI Team. All rights reserved. -// - -#import "QDCommonListViewController.h" - -@interface QDCollectionListViewController : QDCommonListViewController - -@end diff --git a/qmuidemo/Modules/Demos/Components/QDCollectionListViewController.m b/qmuidemo/Modules/Demos/Components/QDCollectionListViewController.m deleted file mode 100644 index 026676e9..00000000 --- a/qmuidemo/Modules/Demos/Components/QDCollectionListViewController.m +++ /dev/null @@ -1,48 +0,0 @@ -// -// QDCollectionListViewController.m -// qmuidemo -// -// Created by QMUI Team on 15/9/24. -// Copyright © 2015年 QMUI Team. All rights reserved. -// - -#import "QDCollectionListViewController.h" -#import "QDCollectionDemoViewController.h" -#import "QDCollectionStackDemoViewController.h" - -@interface QDCollectionListViewController () - -@end - -@implementation QDCollectionListViewController - -- (void)initDataSource { - [super initDataSource]; - self.dataSource = @[@"默认", - @"缩放", - @"旋转"]; -} - -- (void)didSelectCellWithTitle:(NSString *)title { - UIViewController *viewController = nil; - if ([title isEqualToString:@"默认"]) { - viewController = [[QDCollectionDemoViewController alloc] init]; - ((QDCollectionDemoViewController *)viewController).collectionViewLayout.minimumLineSpacing = 20; - } - if ([title isEqualToString:@"缩放"]) { - viewController = [[QDCollectionDemoViewController alloc] initWithLayoutStyle:QMUICollectionViewPagingLayoutStyleScale]; - ((QDCollectionDemoViewController *)viewController).collectionViewLayout.minimumLineSpacing = 0; - } - else if ([title isEqualToString:@"旋转"]) { - viewController = [[QDCollectionDemoViewController alloc] initWithLayoutStyle:QMUICollectionViewPagingLayoutStyleRotation]; - ((QDCollectionDemoViewController *)viewController).collectionViewLayout.minimumLineSpacing = 20; - } - // TODO -// else if ([title isEqualToString:@"叠加"]) { -// viewController = [[QDCollectionStackDemoViewController alloc] init]; -// } - viewController.title = title; - [self.navigationController pushViewController:viewController animated:YES]; -} - -@end diff --git a/qmuidemo/Modules/Demos/Components/QDComponentsViewController.m b/qmuidemo/Modules/Demos/Components/QDComponentsViewController.m index 11166748..dc6e6ab6 100644 --- a/qmuidemo/Modules/Demos/Components/QDComponentsViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDComponentsViewController.m @@ -7,6 +7,7 @@ // #import "QDComponentsViewController.h" +#import "QDCommonListViewController.h" #import "QDNavigationTitleViewController.h" #import "QDEmptyViewController.h" #import "QDGridViewController.h" @@ -14,10 +15,10 @@ #import "QDImagePickerExampleViewController.h" #import "QDMoreOperationViewController.h" #import "QDAssetsManagerViewController.h" -#import "QDImagePreviewExampleViewController.h" #import "QDEmotionsViewController.h" #import "QDPieProgressViewController.h" #import "QDPopupContainerViewController.h" +#import "QDPopupMenuViewController.h" #import "QDModalPresentationViewController.h" #import "QDDialogViewController.h" #import "QDFloatLayoutViewController.h" @@ -25,20 +26,36 @@ #import "QDToastListViewController.h" #import "QDKeyboardViewController.h" #import "QDMarqueeLabelViewController.h" -#import "QDCellKeyCacheViewController.h" #import "QDMultipleDelegatesViewController.h" #import "QDBadgeViewController.h" -#import "QDScrollAnimatorViewController.h" #import "QDConsoleViewController.h" -#import "QDCollectionListViewController.h" +#import "QDThemeViewController.h" +#import "QDCellHeightCacheViewController.h" +#import "QDCellHeightKeyCacheViewController.h" +#import "QDCellSizeKeyCacheViewController.h" +#import "QDImagePreviewViewController1.h" +#import "QDImagePreviewViewController2.h" +#import "QDNavigationBarScrollingAnimatorViewController.h" +#import "QDNavigationBarScrollingSnapAnimatorViewController.h" +#import "QDCollectionDemoViewController.h" +#import "QDCollectionStackDemoViewController.h" +#import "QDLayouterViewController.h" +#import "QDSheetPresentationViewController.h" +#import "QDCheckboxViewController.h" @implementation QDComponentsViewController +- (void)didInitialize { + [super didInitialize]; + self.title = @"Components"; +} + - (void)setupNavigationItems { [super setupNavigationItems]; - self.title = @"Components"; self.navigationItem.rightBarButtonItem = [UIBarButtonItem qmui_itemWithImage:UIImageMake(@"icon_nav_about") target:self action:@selector(handleAboutItemEvent)]; AddAccessibilityLabel(self.navigationItem.rightBarButtonItem, @"打开关于界面"); + + self.qmui_sheetPresentationNavigationBar.backgroundColor = UIColorBlue; } - (void)initDataSource { @@ -47,6 +64,7 @@ - (void)initDataSource { @"QMUIModalPresentationViewController", UIImageMake(@"icon_grid_modal"), @"QMUIDialogViewController", UIImageMake(@"icon_grid_dialog"), @"QMUIMoreOperationController", UIImageMake(@"icon_grid_moreOperation"), + @"QMUISheetPresentation", UIImageMake(@"icon_grid_sheet"), @"QMUINavigationTitleView", UIImageMake(@"icon_grid_titleView"), @"QMUIEmptyView", UIImageMake(@"icon_grid_emptyView"), @"QMUIToastView", UIImageMake(@"icon_grid_toast"), @@ -67,10 +85,14 @@ - (void)initDataSource { @"QMUIScrollAnimator", UIImageMake(@"icon_grid_scrollAnimator"), @"QMUIConsole", UIImageMake(@"icon_grid_console"), @"QMUICollectionViewLayout", UIImageMake(@"icon_grid_collection"), + @"QMUILayouter", UIImageMake(@"icon_grid_floatView"), + @"QMUITheme", UIImageMake(@"icon_grid_theme"), + @"QMUICheckbox", UIImageMake(@"icon_grid_checkbox"), nil]; } - (void)didSelectCellWithTitle:(NSString *)title { + __weak __typeof(self)weakSelf = self; UIViewController *viewController = nil; if ([title isEqualToString:@"QMUINavigationTitleView"]) { viewController = [[QDNavigationTitleViewController alloc] init]; @@ -85,10 +107,47 @@ - (void)didSelectCellWithTitle:(NSString *)title { viewController = [[QDStaticTableViewController alloc] initWithStyle:UITableViewStyleGrouped]; } else if ([title isEqualToString:@"QMUICellKeyCache"]) { - viewController = [[QDCellKeyCacheViewController alloc] init]; + viewController = ({ + QDCommonListViewController *vc = QDCommonListViewController.new; + vc.dataSource = @[ + @"QMUICellHeightCache", + @"QMUICellHeightKeyCache(estimated)", + @"QMUICellSizeKeyCache(暂不能使用)" + ]; + vc.didSelectTitleBlock = ^(NSString *title) { + UIViewController *viewController = nil; + if ([title isEqualToString:@"QMUICellHeightCache"]) { + viewController = [[QDCellHeightCacheViewController alloc] init]; + } else if ([title isEqualToString:@"QMUICellHeightKeyCache(estimated)"]) { + viewController = [[QDCellHeightKeyCacheViewController alloc] init]; + } else if ([title isEqualToString:@"QMUICellSizeKeyCache(暂不能使用)"]) { + viewController = [[QDCellSizeKeyCacheViewController alloc] init]; + } + viewController.title = title; + [weakSelf.navigationController pushViewController:viewController animated:YES]; + }; + vc; + }); } else if ([title isEqualToString:@"QMUIImagePreviewView"]) { - viewController = [[QDImagePreviewExampleViewController alloc] init]; + viewController = ({ + QDCommonListViewController *vc = QDCommonListViewController.new; + vc.dataSource = @[ + NSStringFromClass([QMUIImagePreviewView class]), + NSStringFromClass([QMUIImagePreviewViewController class]) + ]; + vc.didSelectTitleBlock = ^(NSString *title) { + UIViewController *viewController = nil; + if ([title isEqualToString:NSStringFromClass([QMUIImagePreviewView class])]) { + viewController = [[QDImagePreviewViewController1 alloc] init]; + } else if ([title isEqualToString:NSStringFromClass([QMUIImagePreviewViewController class])]) { + viewController = [[QDImagePreviewViewController2 alloc] init]; + viewController.title = title; + } + [weakSelf.navigationController pushViewController:viewController animated:YES]; + }; + vc; + }); } else if ([title isEqualToString:@"QMUIPickingImage"]) { viewController = [[QDImagePickerExampleViewController alloc] init]; @@ -112,7 +171,24 @@ - (void)didSelectCellWithTitle:(NSString *)title { viewController = [[QDPieProgressViewController alloc] init]; } else if ([title isEqualToString:@"QMUIPopupContainerView"]) { - viewController = [[QDPopupContainerViewController alloc] init]; + viewController = ({ + QDCommonListViewController *vc = QDCommonListViewController.new; + vc.dataSource = @[ + NSStringFromClass([QMUIPopupContainerView class]), + NSStringFromClass([QMUIPopupMenuView class]) + ]; + vc.didSelectTitleBlock = ^(NSString *title) { + UIViewController *viewController = nil; + if ([title isEqualToString:NSStringFromClass([QMUIPopupContainerView class])]) { + viewController = [[QDPopupContainerViewController alloc] init]; + } else if ([title isEqualToString:NSStringFromClass([QMUIPopupMenuView class])]) { + viewController = [[QDPopupMenuViewController alloc] init]; + } + viewController.title = title; + [weakSelf.navigationController pushViewController:viewController animated:YES]; + }; + vc; + }); } else if ([title isEqualToString:@"QMUIModalPresentationViewController"]) { viewController = [[QDModalPresentationViewController alloc] initWithStyle:UITableViewStyleGrouped]; @@ -133,13 +209,71 @@ - (void)didSelectCellWithTitle:(NSString *)title { viewController = [[QDBadgeViewController alloc] init]; } else if ([title isEqualToString:@"QMUIScrollAnimator"]) { - viewController = [[QDScrollAnimatorViewController alloc] init]; + viewController = ({ + QDCommonListViewController *vc = QDCommonListViewController.new; + vc.dataSource = @[ + NSStringFromClass([QMUINavigationBarScrollingAnimator class]), + NSStringFromClass([QMUINavigationBarScrollingSnapAnimator class]) + ]; + vc.didSelectTitleBlock = ^(NSString *title) { + UIViewController *viewController = nil; + if ([title isEqualToString:NSStringFromClass([QMUINavigationBarScrollingAnimator class])]) { + viewController = [[QDNavigationBarScrollingAnimatorViewController alloc] init]; + } else if ([title isEqualToString:NSStringFromClass([QMUINavigationBarScrollingSnapAnimator class])]) { + viewController = [[QDNavigationBarScrollingSnapAnimatorViewController alloc] init]; + } + viewController.title = title; + [weakSelf.navigationController pushViewController:viewController animated:YES]; + }; + vc; + }); } else if ([title isEqualToString:@"QMUIConsole"]) { viewController = [[QDConsoleViewController alloc] init]; } else if ([title isEqualToString:@"QMUICollectionViewLayout"]) { - viewController = [[QDCollectionListViewController alloc] init]; + viewController = ({ + QDCommonListViewController *vc = QDCommonListViewController.new; + vc.dataSource = @[ + @"默认", + @"缩放", + @"旋转" + ]; + vc.didSelectTitleBlock = ^(NSString *title) { + UIViewController *viewController = nil; + if ([title isEqualToString:@"默认"]) { + viewController = [[QDCollectionDemoViewController alloc] init]; + ((QDCollectionDemoViewController *)viewController).collectionViewLayout.minimumLineSpacing = 20; + } + if ([title isEqualToString:@"缩放"]) { + viewController = [[QDCollectionDemoViewController alloc] initWithLayoutStyle:QMUICollectionViewPagingLayoutStyleScale]; + ((QDCollectionDemoViewController *)viewController).collectionViewLayout.minimumLineSpacing = 0; + } + else if ([title isEqualToString:@"旋转"]) { + viewController = [[QDCollectionDemoViewController alloc] initWithLayoutStyle:QMUICollectionViewPagingLayoutStyleRotation]; + ((QDCollectionDemoViewController *)viewController).collectionViewLayout.minimumLineSpacing = 20; + } + // TODO + // else if ([title isEqualToString:@"叠加"]) { + // viewController = [[QDCollectionStackDemoViewController alloc] init]; + // } + viewController.title = title; + [weakSelf.navigationController pushViewController:viewController animated:YES]; + }; + vc; + }); + } + else if ([title isEqualToString:@"QMUILayouter"]) { + viewController = [[QDLayouterViewController alloc] init]; + } + else if ([title isEqualToString:@"QMUITheme"]) { + viewController = [[QDThemeViewController alloc] init]; + } + else if ([title isEqualToString:@"QMUISheetPresentation"]) { + viewController = [[QDSheetPresentationViewController alloc] init]; + } + else if ([title isEqualToString:@"QMUICheckbox"]) { + viewController = [[QDCheckboxViewController alloc] init]; } viewController.title = title; [self.navigationController pushViewController:viewController animated:YES]; diff --git a/qmuidemo/Modules/Demos/Components/QDConsoleViewController.m b/qmuidemo/Modules/Demos/Components/QDConsoleViewController.m index 56f0ec1e..5697cb85 100644 --- a/qmuidemo/Modules/Demos/Components/QDConsoleViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDConsoleViewController.m @@ -20,18 +20,20 @@ @implementation QDConsoleViewController - (void)initSubviews { [super initSubviews]; self.printLogButton = [QDUIHelper generateLightBorderedButton]; + self.printLogButton.qmui_preventsRepeatedTouchUpInsideEvent = NO; [self.printLogButton setTitle:@"打印一条日志" forState:UIControlStateNormal]; [self.printLogButton addTarget:self action:@selector(handlePrintLogButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:self.printLogButton]; self.printMultipleLogButton = [QDUIHelper generateLightBorderedButton]; + self.printMultipleLogButton.qmui_preventsRepeatedTouchUpInsideEvent = NO; [self.printMultipleLogButton setTitle:@"打印多种 Level/Name" forState:UIControlStateNormal]; [self.printMultipleLogButton addTarget:self action:@selector(handlePrintMulitipleEvent:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:self.printMultipleLogButton]; self.tipsLabel = [[UILabel alloc] init]; self.tipsLabel.numberOfLines = 0; - NSMutableAttributedString *tips = [[NSMutableAttributedString alloc] initWithString:@"[QMUIConsole log:xxx] 可以直接在屏幕上显示日志,通常用于一些重要信息但又不适合用 NSAssert 提示的场景。可通过长按小圆钮关闭控制台。\n支持搜索或按 Level、Name 过滤日志,可以通过配置表 ShouldPrintQMUIWarnLogToConsole 让 QMUILogWarn() 的内容也自动显示到 QMUIConsole 里(默认仅在 DEBUG 下打开)。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColorGray6, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]}]; + NSMutableAttributedString *tips = [[NSMutableAttributedString alloc] initWithString:@"[QMUIConsole log:xxx] 可以直接在屏幕上显示日志,通常用于一些重要信息但又不适合用 NSAssert 提示的场景。可通过长按小圆钮关闭控制台。\n支持搜索或按 Level、Name 过滤日志,可以通过配置表 ShouldPrintQMUIWarnLogToConsole 让 QMUILogWarn() 的内容也自动显示到 QMUIConsole 里(默认仅在 DEBUG 下打开)。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]}]; NSDictionary *codeAttributes = CodeAttributes(12); [tips.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { [tips addAttributes:codeAttributes range:codeRange]; @@ -42,7 +44,7 @@ - (void)initSubviews { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - UIEdgeInsets paddings = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator + 24, 24 + self.view.qmui_safeAreaInsets.left, 24 + self.view.qmui_safeAreaInsets.bottom, 24 + self.view.qmui_safeAreaInsets.right); + UIEdgeInsets paddings = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator + 24, 24 + self.view.safeAreaInsets.left, 24 + self.view.safeAreaInsets.bottom, 24 + self.view.safeAreaInsets.right); CGFloat buttonSpacing = 16; self.printLogButton.frame = CGRectMake(paddings.left, paddings.top, CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(paddings), CGRectGetHeight(self.printLogButton.frame)); self.printMultipleLogButton.frame = CGRectSetY(self.printLogButton.frame, CGRectGetMaxY(self.printLogButton.frame) + buttonSpacing); @@ -56,7 +58,7 @@ - (void)handlePrintLogButtonEvent:(QMUIButton *)button { // 支持 NSAttributedString 自定义 log 样式 // [QMUIConsole log:[[NSAttributedString alloc] initWithString:@"NSAttributedString log" attributes:({ // NSMutableDictionary *attributes = [QMUIConsole appearance].textAttributes.mutableCopy; -// attributes[NSForegroundColorAttributeName] = [QDThemeManager sharedInstance].currentTheme.themeTintColor; +// attributes[NSForegroundColorAttributeName] = UIColor.qd_tintColor; // attributes; // })]]; @@ -68,7 +70,7 @@ - (void)handlePrintMulitipleEvent:(QMUIButton *)button { [QMUIConsole logWithLevel:@"Info" name:@"QMUIBadge" logString:@"QMUIBadge's info log"]; [QMUIConsole logWithLevel:@"Warn" name:@"QMUITableView" logString:[[NSAttributedString alloc] initWithString:@"QMUITableView's warn log" attributes:({ NSMutableDictionary *attributes = [QMUIConsole appearance].textAttributes.mutableCopy; - attributes[NSForegroundColorAttributeName] = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + attributes[NSForegroundColorAttributeName] = [QDCommonUI randomThemeColor]; attributes; })]]; [QMUIConsole logWithLevel:@"Error" name:@"QMUIButton" logString:button]; diff --git a/qmuidemo/Modules/Demos/Components/QDDialogViewController.m b/qmuidemo/Modules/Demos/Components/QDDialogViewController.m index 1e8ecb23..bdcc2e05 100644 --- a/qmuidemo/Modules/Demos/Components/QDDialogViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDDialogViewController.m @@ -114,8 +114,9 @@ - (void)showNormalDialogViewController { - (void)showAppearanceDialogViewController { QMUIDialogViewController *dialogViewController = [[QMUIDialogViewController alloc] init]; dialogViewController.title = @"标题"; + dialogViewController.titleView.subtitle = @"副标题"; UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 50)]; - contentView.backgroundColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + contentView.backgroundColor = UIColor.qd_tintColor; UILabel *label = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorWhite]; label.text = @"自定义contentView"; [label sizeToFit]; @@ -128,12 +129,18 @@ - (void)showAppearanceDialogViewController { [aDialogViewController hide]; }]; - // 自定义样式 - dialogViewController.headerViewBackgroundColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + // === 自定义样式 === + dialogViewController.headerViewBackgroundColor = UIColor.qd_tintColor; dialogViewController.headerSeparatorColor = nil; dialogViewController.footerSeparatorColor = nil; + + // titleView + dialogViewController.titleView.style = QMUINavigationTitleViewStyleSubTitleVertical; + dialogViewController.titleView.verticalTitleFont = UIFontBoldMake(17); dialogViewController.titleTintColor = UIColorWhite; - dialogViewController.titleView.horizontalTitleFont = UIFontBoldMake(17); + dialogViewController.titleLabelTextColor = nil; + dialogViewController.subTitleLabelTextColor = nil; + dialogViewController.buttonHighlightedBackgroundColor = [dialogViewController.headerViewBackgroundColor qmui_colorWithAlphaAddedToWhite:.3]; NSMutableDictionary *buttonTitleAttributes = dialogViewController.buttonTitleAttributes.mutableCopy; buttonTitleAttributes[NSForegroundColorAttributeName] = dialogViewController.headerViewBackgroundColor; diff --git a/qmuidemo/Modules/Demos/Components/QDDynamicHeightTableViewCell.m b/qmuidemo/Modules/Demos/Components/QDDynamicHeightTableViewCell.m index b187f616..6c19586e 100644 --- a/qmuidemo/Modules/Demos/Components/QDDynamicHeightTableViewCell.m +++ b/qmuidemo/Modules/Demos/Components/QDDynamicHeightTableViewCell.m @@ -23,14 +23,14 @@ - (void)didInitializeWithStyle:(UITableViewCellStyle)style { _avatarImageView = [[UIImageView alloc] initWithImage:avatarImage]; [self.contentView addSubview:self.avatarImageView]; - _nameLabel = [[UILabel alloc] qmui_initWithFont:UIFontBoldMake(16) textColor:UIColorGray2]; + _nameLabel = [[UILabel alloc] qmui_initWithFont:UIFontBoldMake(16) textColor:UIColor.qd_mainTextColor]; [self.contentView addSubview:self.nameLabel]; - _contentLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(17) textColor:UIColorGray1]; + _contentLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(17) textColor:UIColor.qd_mainTextColor]; self.contentLabel.numberOfLines = 0; [self.contentView addSubview:self.contentLabel]; - _timeLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(13) textColor:UIColorGray]; + _timeLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(13) textColor:UIColor.qd_descriptionTextColor]; [self.contentView addSubview:self.timeLabel]; } @@ -50,8 +50,9 @@ - (NSAttributedString *)attributeStringWithString:(NSString *)textString lineHei } - (CGSize)sizeThatFits:(CGSize)size { - CGSize resultSize = CGSizeMake(size.width, 0); - CGFloat contentLabelWidth = size.width - UIEdgeInsetsGetHorizontalValue(kInsets); + CGFloat contentWidth = CGRectGetWidth(self.contentView.bounds); + CGSize resultSize = CGSizeMake(contentWidth, 0); + CGFloat contentLabelWidth = contentWidth - UIEdgeInsetsGetHorizontalValue(kInsets); CGFloat resultHeight = UIEdgeInsetsGetHorizontalValue(kInsets) + CGRectGetHeight(self.avatarImageView.bounds) + kAvatarMarginBottom; diff --git a/qmuidemo/Modules/Demos/Components/QDEmotionsViewController.m b/qmuidemo/Modules/Demos/Components/QDEmotionsViewController.m index 649adf56..14f499e5 100644 --- a/qmuidemo/Modules/Demos/Components/QDEmotionsViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDEmotionsViewController.m @@ -25,7 +25,7 @@ - (void)initSubviews { self.descriptionLabel = [[UILabel alloc] init]; self.descriptionLabel.numberOfLines = 0; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"本界面以 QMUIEmotionInputManager 为例,展示 QMUIEmotionView 的功能,若需查看 QMUIEmotionView 的使用方式,请参考 QMUIEmotionInputManager。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColorGray1, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:22]}]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"本界面以 QMUIEmotionInputManager 为例,展示 QMUIEmotionView 的功能,若需查看 QMUIEmotionView 的使用方式,请参考 QMUIEmotionInputManager。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColor.qd_mainTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:22]}]; NSDictionary *codeAttributes = CodeAttributes(16); [attributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { [attributedString addAttributes:codeAttributes range:codeRange]; @@ -35,10 +35,11 @@ - (void)initSubviews { self.toolbar = [[UIView alloc] init]; self.toolbar.qmui_borderPosition = QMUIViewBorderPositionTop; - self.toolbar.backgroundColor = UIColorWhite; + self.toolbar.backgroundColor = UIColorForBackground; [self.view addSubview:self.toolbar]; self.textField = [[QMUITextField alloc] init]; + self.textField.backgroundColor = nil; self.textField.placeholder = @"请输入文字"; self.textField.delegate = self; @@ -79,6 +80,12 @@ - (void)initSubviews { self.emotionInputManager.emotionView.alpha = 0; } +- (void)setupNavigationItems { + [super setupNavigationItems]; + self.navigationItem.rightBarButtonItem = [UIBarButtonItem qmui_itemWithTitle:@"切换布局" target:self action:@selector(handleChangeAlignmentEvent:)]; +} + + - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; self.textField.qmui_keyboardManager.delegateEnabled = NO; @@ -102,21 +109,21 @@ - (void)viewDidAppear:(BOOL)animated { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - UIEdgeInsets padding = UIEdgeInsetsMake(20, 20 + self.view.qmui_safeAreaInsets.left, 20, 20 + self.view.qmui_safeAreaInsets.right); + UIEdgeInsets padding = UIEdgeInsetsMake(20, 20 + self.view.safeAreaInsets.left, 20, 20 + self.view.safeAreaInsets.right); CGFloat contentWidth = CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding); self.descriptionLabel.frame = CGRectFlatMake(padding.left, self.qmui_navigationBarMaxYInViewCoordinator + padding.top, contentWidth, QMUIViewSelfSizingHeight); CGFloat toolbarHeight = 56; - CGFloat emotionViewHeight = 232; + CGFloat emotionViewHeight = 232 + self.view.safeAreaInsets.bottom; if (self.keyboardVisible) { self.toolbar.frame = CGRectMake(0, CGRectGetHeight(self.view.bounds) - self.keyboardHeight - toolbarHeight, CGRectGetWidth(self.view.bounds), toolbarHeight); self.emotionInputManager.emotionView.frame = CGRectMake(0, CGRectGetMaxY(self.toolbar.frame), CGRectGetWidth(self.view.bounds), emotionViewHeight); } else { - self.emotionInputManager.emotionView.frame = CGRectMake(0, CGRectGetHeight(self.view.bounds) - self.view.qmui_safeAreaInsets.bottom - emotionViewHeight, CGRectGetWidth(self.view.bounds), emotionViewHeight); + self.emotionInputManager.emotionView.frame = CGRectMake(0, CGRectGetHeight(self.view.bounds) - emotionViewHeight, CGRectGetWidth(self.view.bounds), emotionViewHeight); self.toolbar.frame = CGRectMake(0, CGRectGetMinY(self.emotionInputManager.emotionView.frame) - toolbarHeight, CGRectGetWidth(self.view.bounds), toolbarHeight); } - UIEdgeInsets toolbarPadding = UIEdgeInsetsConcat(UIEdgeInsetsMake(2, 8, 2, 8), self.toolbar.qmui_safeAreaInsets); + UIEdgeInsets toolbarPadding = UIEdgeInsetsConcat(UIEdgeInsetsMake(2, 8, 2, 8), self.toolbar.safeAreaInsets); self.textField.frame = CGRectMake(toolbarPadding.left, toolbarPadding.top, CGRectGetWidth(self.toolbar.bounds) - UIEdgeInsetsGetHorizontalValue(toolbarPadding), CGRectGetHeight(self.toolbar.bounds) - UIEdgeInsetsGetVerticalValue(toolbarPadding)); } @@ -128,6 +135,14 @@ - (BOOL)shouldHideKeyboardWhenTouchInView:(UIView *)view { return YES; } +- (void)handleChangeAlignmentEvent:(id)sender { + BOOL verticalAlignment = !self.emotionInputManager.emotionView.verticalAlignment; + self.emotionInputManager.emotionView.deleteButton.contentEdgeInsets = verticalAlignment ? UIEdgeInsetsMake(0, 20, 0, 20) : UIEdgeInsetsZero; + self.emotionInputManager.emotionView.deleteButtonBackgroundColor = verticalAlignment ? UIColorBlue : nil; + self.emotionInputManager.emotionView.deleteButtonImage = verticalAlignment ? [[QMUIHelper imageWithName:@"QMUI_emotion_delete"] qmui_imageWithTintColor:UIColor.whiteColor] : [QMUIHelper imageWithName:@"QMUI_emotion_delete"]; + self.emotionInputManager.emotionView.verticalAlignment = verticalAlignment; +} + #pragma mark - - (BOOL)textFieldShouldEndEditing:(UITextField *)textField { diff --git a/qmuidemo/Modules/Demos/Components/QDEmptyViewController.m b/qmuidemo/Modules/Demos/Components/QDEmptyViewController.m index 51b8701e..97584f42 100644 --- a/qmuidemo/Modules/Demos/Components/QDEmptyViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDEmptyViewController.m @@ -10,7 +10,7 @@ @interface QDEmptyViewController () -@property(nonatomic, assign) UIStatusBarStyle statusBarStyle; +@property(nonatomic, strong) UITapGestureRecognizer *tapGesture; @end @implementation QDEmptyViewController @@ -18,18 +18,27 @@ @implementation QDEmptyViewController - (instancetype)initWithStyle:(UITableViewStyle)style { if (self = [super initWithStyle:style]) { self.shouldShowSearchBar = YES; - self.statusBarStyle = [super preferredStatusBarStyle]; } return self; } -- (UIStatusBarStyle)preferredStatusBarStyle { - return self.statusBarStyle; +#pragma mark - 工具方法 + +- (void)showEmptyView { + [super showEmptyView]; + // 如果对 emptyView 有自定义的需求,可以重写 showEmptyView 方法,在这里获取 self.emptyView 进行配置 + if (!self.tapGesture) { + self.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; + [self.emptyView addGestureRecognizer:self.tapGesture]; + } + self.tapGesture.enabled = YES; } -#pragma mark - 工具方法 +- (void)handleTapGesture:(UITapGestureRecognizer *)tap { + [self reload]; +} -- (void)reload:(id)sender { +- (void)reload { [self hideEmptyView]; [self.tableView reloadData]; } @@ -68,23 +77,12 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } else if (row == 1) { [self showEmptyViewWithText:@"联系人为空" detailText:@"请到设置-隐私查看你的联系人权限设置" buttonTitle:nil buttonAction:NULL]; } else if (row == 2) { - [self showEmptyViewWithText:@"请求失败" detailText:@"请检查网络连接" buttonTitle:@"重试" buttonAction:@selector(reload:)]; + [self showEmptyViewWithText:@"请求失败" detailText:@"请检查网络连接" buttonTitle:@"重试" buttonAction:@selector(reload)]; + self.tapGesture.enabled = NO; } else if (row == 3) { [self showEmptyViewWithImage:UIImageMake(@"icon_grid_emptyView") text:nil detailText:@"图片间距可通过imageInsets来调整" buttonTitle:nil buttonAction:NULL]; } [self.tableView reloadData]; } -#pragma mark - - -- (void)willPresentSearchController:(QMUISearchController *)searchController { - self.statusBarStyle = UIStatusBarStyleDefault; - [self setNeedsStatusBarAppearanceUpdate]; -} - -- (void)willDismissSearchController:(QMUISearchController *)searchController { - self.statusBarStyle = [super preferredStatusBarStyle]; - [self setNeedsStatusBarAppearanceUpdate]; -} - @end diff --git a/qmuidemo/Modules/Demos/Components/QDFloatLayoutViewController.m b/qmuidemo/Modules/Demos/Components/QDFloatLayoutViewController.m index fd41a31c..8c9b16d0 100644 --- a/qmuidemo/Modules/Demos/Components/QDFloatLayoutViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDFloatLayoutViewController.m @@ -19,7 +19,7 @@ - (void)initSubviews { [super initSubviews]; self.floatLayoutView = [[QMUIFloatLayoutView alloc] init]; self.floatLayoutView.padding = UIEdgeInsetsMake(12, 12, 12, 12); - self.floatLayoutView.itemMargins = UIEdgeInsetsMake(0, 0, 10, 10); + self.floatLayoutView.itemMargins = UIEdgeInsetsMake(10, 10, 10, 10); self.floatLayoutView.minimumItemSize = CGSizeMake(69, 29);// 以2个字的按钮作为最小宽度 self.floatLayoutView.layer.borderWidth = PixelOne; self.floatLayoutView.layer.borderColor = UIColorSeparator.CGColor; @@ -27,8 +27,7 @@ - (void)initSubviews { NSArray *suggestions = @[@"东野圭吾", @"三体", @"爱", @"红楼梦", @"理智与情感", @"读书热榜", @"免费榜"]; for (NSInteger i = 0; i < suggestions.count; i++) { - QMUIGhostButton *button = [[QMUIGhostButton alloc] init]; - button.ghostColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + QMUIButton *button = [QDUIHelper generateGhostButtonWithColor:UIColor.qd_tintColor]; [button setTitle:suggestions[i] forState:UIControlStateNormal]; button.titleLabel.font = UIFontMake(14); button.contentEdgeInsets = UIEdgeInsetsMake(6, 20, 6, 20); @@ -38,7 +37,7 @@ - (void)initSubviews { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - UIEdgeInsets padding = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator + 36, 24, 36, 24); + UIEdgeInsets padding = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator + 36, 24 + self.view.safeAreaInsets.left, 36, 24 + self.view.safeAreaInsets.right); self.floatLayoutView.frame = CGRectMake(padding.left, padding.top, CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding), QMUIViewSelfSizingHeight); } diff --git a/qmuidemo/Modules/Demos/Components/QDGridViewController.m b/qmuidemo/Modules/Demos/Components/QDGridViewController.m index a337b7c0..c3e6614d 100644 --- a/qmuidemo/Modules/Demos/Components/QDGridViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDGridViewController.m @@ -36,14 +36,14 @@ - (void)initSubviews { } self.tipsLabel = [[UILabel alloc] init]; - self.tipsLabel.attributedText = [[NSAttributedString alloc] initWithString:@"适用于那种要将若干个 UIView 以九宫格的布局摆放的情况,支持显示 item 之间的分隔线。\n注意当 QMUIGridView 宽度发生较大变化时(例如横屏旋转),并不会自动增加列数,这种场景要么自己重新设置 columnCount,要么改为用 UICollectionView 实现。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColorGray6, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:18]}]; + self.tipsLabel.attributedText = [[NSAttributedString alloc] initWithString:@"适用于那种要将若干个 UIView 以九宫格的布局摆放的情况,支持显示 item 之间的分隔线。\n注意当 QMUIGridView 宽度发生较大变化时(例如横屏旋转),并不会自动增加列数,这种场景要么自己重新设置 columnCount,要么改为用 UICollectionView 实现。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:18]}]; self.tipsLabel.numberOfLines = 0; [self.view addSubview:self.tipsLabel]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - UIEdgeInsets padding = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.qmui_safeAreaInsets.left, 24 + self.view.qmui_safeAreaInsets.bottom, 24 + self.view.qmui_safeAreaInsets.right); + UIEdgeInsets padding = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.safeAreaInsets.left, 24 + self.view.safeAreaInsets.bottom, 24 + self.view.safeAreaInsets.right); CGFloat contentWidth = CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding); self.gridView.frame = CGRectMake(padding.left, padding.top, contentWidth, QMUIViewSelfSizingHeight); diff --git a/qmuidemo/Modules/Demos/Components/QDImagePreviewExampleViewController.h b/qmuidemo/Modules/Demos/Components/QDImagePreviewExampleViewController.h deleted file mode 100644 index 385667c6..00000000 --- a/qmuidemo/Modules/Demos/Components/QDImagePreviewExampleViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDImagePreviewExampleViewController.h -// qmuidemo -// -// Created by QMUI Team on 2016/12/6. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QDCommonListViewController.h" - -@interface QDImagePreviewExampleViewController : QDCommonListViewController - -@end diff --git a/qmuidemo/Modules/Demos/Components/QDImagePreviewExampleViewController.m b/qmuidemo/Modules/Demos/Components/QDImagePreviewExampleViewController.m deleted file mode 100644 index de59520f..00000000 --- a/qmuidemo/Modules/Demos/Components/QDImagePreviewExampleViewController.m +++ /dev/null @@ -1,31 +0,0 @@ -// -// QDImagePreviewExampleViewController.m -// qmuidemo -// -// Created by QMUI Team on 2016/12/6. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QDImagePreviewExampleViewController.h" -#import "QDImagePreviewViewController1.h" -#import "QDImagePreviewViewController2.h" - -@implementation QDImagePreviewExampleViewController - -- (void)initDataSource { - self.dataSource = @[NSStringFromClass([QMUIImagePreviewView class]), - NSStringFromClass([QMUIImagePreviewViewController class])]; -} - -- (void)didSelectCellWithTitle:(NSString *)title { - UIViewController *viewController = nil; - if ([title isEqualToString:NSStringFromClass([QMUIImagePreviewView class])]) { - viewController = [[QDImagePreviewViewController1 alloc] init]; - } else if ([title isEqualToString:NSStringFromClass([QMUIImagePreviewViewController class])]) { - viewController = [[QDImagePreviewViewController2 alloc] init]; - viewController.title = title; - } - [self.navigationController pushViewController:viewController animated:YES]; -} - -@end diff --git a/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController1.m b/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController1.m index 18c58723..dcc36d37 100644 --- a/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController1.m +++ b/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController1.m @@ -19,8 +19,6 @@ @implementation QDImagePreviewViewController1 - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - self.automaticallyAdjustsScrollViewInsets = NO; - self.images = @[UIImageMake(@"image0"), UIImageMake(@"image1"), UIImageMake(@"image2"), diff --git a/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController2.m b/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController2.m index dea73034..8a0c7e81 100644 --- a/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController2.m +++ b/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController2.m @@ -12,7 +12,7 @@ @interface QDImagePreviewViewController2 () @property(nonatomic, strong) QMUIImagePreviewViewController *imagePreviewViewController; @property(nonatomic, strong) NSArray *images; -@property(nonatomic, strong) UIButton *imageButton; +@property(nonatomic, strong) QMUIFloatLayoutView *floatLayoutView; @property(nonatomic, strong) UILabel *tipsLabel; @end @@ -26,8 +26,7 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB UIImageMake(@"image2"), UIImageMake(@"image3"), UIImageMake(@"image4"), - UIImageMake(@"image5"), - UIImageMake(@"image6")]; + UIImageMake(@"image5")]; } return self; } @@ -35,27 +34,39 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB - (void)initSubviews { [super initSubviews]; - self.imageButton = [[UIButton alloc] init]; - self.imageButton.imageView.contentMode = UIViewContentModeScaleAspectFill; - [self.imageButton setImage:self.images[2] forState:UIControlStateNormal]; - [self.imageButton addTarget:self action:@selector(handleImageButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - self.imageButton.clipsToBounds = YES; - [self.view addSubview:self.imageButton]; + self.floatLayoutView = [[QMUIFloatLayoutView alloc] init]; + self.floatLayoutView.itemMargins = UIEdgeInsetsMake(PixelOne, PixelOne, 0, 0); + for (UIImage *image in self.images) { + QMUIButton *button = [[QMUIButton alloc] init]; + button.imageView.contentMode = UIViewContentModeScaleAspectFill; + [button setImage:image forState:UIControlStateNormal]; + [button addTarget:self action:@selector(handleImageButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.floatLayoutView addSubview:button]; + } + [self.view addSubview:self.floatLayoutView]; self.tipsLabel = [[UILabel alloc] init]; - self.tipsLabel.attributedText = [[NSAttributedString alloc] initWithString:@"点击图片后可左右滑动,期间也可尝试横竖屏" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColorGray6, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:16 lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentCenter]}]; + self.tipsLabel.font = UIFontMake(12); + self.tipsLabel.textColor = UIColor.qd_descriptionTextColor; + self.tipsLabel.textAlignment = NSTextAlignmentCenter; + self.tipsLabel.qmui_lineHeight = 16; self.tipsLabel.numberOfLines = 0; + self.tipsLabel.text = @"点击图片后可左右滑动,期间也可尝试横竖屏"; [self.view addSubview:self.tipsLabel]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - CGSize imageButtonSize = CGSizeMake(self.images.firstObject.size.height / 2, self.images.firstObject.size.height / 2); - self.imageButton.frame = CGRectFlatMake(CGFloatGetCenter(CGRectGetWidth(self.view.bounds), imageButtonSize.width), self.qmui_navigationBarMaxYInViewCoordinator + 24, imageButtonSize.width, imageButtonSize.height); - self.imageButton.layer.cornerRadius = CGRectGetHeight(self.imageButton.frame) / 2; + UIEdgeInsets margins = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.safeAreaInsets.left, 24, 24 + self.view.safeAreaInsets.right); + CGFloat contentWidth = self.view.qmui_width - UIEdgeInsetsGetHorizontalValue(margins); + NSInteger column = IS_IPAD || IS_LANDSCAPE ? self.images.count : 3; + CGFloat imageWidth = contentWidth / column - (column - 1) * UIEdgeInsetsGetHorizontalValue(self.floatLayoutView.itemMargins); + self.floatLayoutView.minimumItemSize = CGSizeMake(imageWidth, imageWidth); + self.floatLayoutView.maximumItemSize = self.floatLayoutView.minimumItemSize; + self.floatLayoutView.frame = CGRectMake(margins.left, margins.top, contentWidth, QMUIViewSelfSizingHeight); - self.tipsLabel.frame = CGRectFlatMake(32, CGRectGetMaxY(self.imageButton.frame) + 8, CGRectGetWidth(self.view.bounds) - 32 * 2, QMUIViewSelfSizingHeight); + self.tipsLabel.frame = CGRectFlatMake(margins.left, CGRectGetMaxY(self.floatLayoutView.frame) + 16, contentWidth, QMUIViewSelfSizingHeight); } - (void)handleImageButtonEvent:(UIButton *)button { @@ -63,26 +74,26 @@ - (void)handleImageButtonEvent:(UIButton *)button { self.imagePreviewViewController = [[QMUIImagePreviewViewController alloc] init]; self.imagePreviewViewController.presentingStyle = QMUIImagePreviewViewControllerTransitioningStyleZoom;// 将 present 动画改为 zoom,也即从某个位置放大到屏幕中央。默认样式为 fade。 self.imagePreviewViewController.imagePreviewView.delegate = self;// 将内部的图片查看器 delegate 指向当前 viewController,以获取要查看的图片数据 - self.imagePreviewViewController.imagePreviewView.currentImageIndex = 2;// 默认展示的图片 index - - __weak __typeof(self)weakSelf = self; - - // 如果使用 zoom 动画,则需要在 sourceImageView 里返回一个 UIView,由这个 UIView 的布局位置决定动画的起点/终点,如果用 fade 则不需要使用 sourceImageView。 - // 另外当 sourceImageView 返回 nil 时会强制使用 fade 动画,常见的使用场景是 present 时 sourceImageView 还在屏幕内,但 dismiss 时 sourceImageView 已经不在可视区域,即可通过返回 nil 来改用 fade 动画。 - self.imagePreviewViewController.sourceImageView = ^UIView *{ - return weakSelf.imageButton; - }; // 当需要在退出大图预览时做一些事情的时候,可配合 UIViewController (QMUI) 的 qmui_visibleStateDidChangeBlock 来实现。 + __weak __typeof(self)weakSelf = self; self.imagePreviewViewController.qmui_visibleStateDidChangeBlock = ^(QMUIImagePreviewViewController *viewController, QMUIViewControllerVisibleState visibleState) { if (visibleState == QMUIViewControllerWillDisappear) { - UIImage *currentImage = [viewController.imagePreviewView zoomImageViewAtIndex:viewController.imagePreviewView.currentImageIndex].image; - if (currentImage) { - [weakSelf.imageButton setImage:currentImage forState:UIControlStateNormal]; - } + NSInteger exitAtIndex = viewController.imagePreviewView.currentImageIndex; + weakSelf.tipsLabel.text = [NSString stringWithFormat:@"浏览到第%@张就退出了", @(exitAtIndex + 1)]; } }; } + + NSInteger buttonIndex = [self.floatLayoutView.subviews indexOfObject:button]; + self.imagePreviewViewController.imagePreviewView.currentImageIndex = buttonIndex;// 默认展示的图片 index + + // 如果使用 zoom 动画,则需要在 sourceImageView 里返回一个 UIView,由这个 UIView 的布局位置决定动画的起点/终点,如果用 fade 则不需要使用 sourceImageView。 + // 另外当 sourceImageView 返回 nil 时会强制使用 fade 动画,常见的使用场景是 present 时 sourceImageView 还在屏幕内,但 dismiss 时 sourceImageView 已经不在可视区域,即可通过返回 nil 来改用 fade 动画。 + self.imagePreviewViewController.sourceImageView = ^UIView *{ + return button; + }; + [self presentViewController:self.imagePreviewViewController animated:YES completion:nil]; } @@ -113,6 +124,14 @@ - (QMUIImagePreviewMediaType)imagePreviewView:(QMUIImagePreviewView *)imagePrevi return QMUIImagePreviewMediaTypeImage; } +- (void)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView didScrollToIndex:(NSUInteger)index { + // 由于进入大图查看模式后可以左右滚动切换图片,最终退出时要退出到当前大图所对应的小图那,所以需要在适当的时机(这里选择 imagePreviewView:didScrollToIndex:)更新 sourceImageView 的值 + __weak __typeof(self)weakSelf = self; + self.imagePreviewViewController.sourceImageView = ^UIView *{ + return weakSelf.floatLayoutView.subviews[index]; + }; +} + #pragma mark - - (void)singleTouchInZoomingImageView:(QMUIZoomImageView *)zoomImageView location:(CGPoint)location { diff --git a/qmuidemo/Modules/Demos/Components/QDKeyboardViewController.m b/qmuidemo/Modules/Demos/Components/QDKeyboardViewController.m index 86fa50c1..ec0c0992 100644 --- a/qmuidemo/Modules/Demos/Components/QDKeyboardViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDKeyboardViewController.m @@ -45,8 +45,9 @@ - (void)initSubviews { [self.view addSubview:self.maskControl]; _containerView = [[UIView alloc] init]; - self.containerView.backgroundColor = UIColorWhite; + self.containerView.backgroundColor = UIColorForBackground; self.containerView.layer.cornerRadius = 8; + self.containerView.layer.maskedCorners = kCALayerMinXMinYCorner|kCALayerMaxXMinYCorner; [self.view addSubview:self.containerView]; _textView = [[QMUITextView alloc] init]; @@ -55,10 +56,16 @@ - (void)initSubviews { self.textView.textContainerInset = UIEdgeInsetsMake(16, 12, 16, 12); self.textView.layer.cornerRadius = 8; self.textView.clipsToBounds = YES; + self.textView.backgroundColor = UIColor.qd_backgroundColorLighten; [self.containerView addSubview:self.textView]; _toolbarView = [[UIView alloc] init]; - self.toolbarView.backgroundColor = UIColorMake(246, 246, 246); + self.toolbarView.backgroundColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject * _Nullable theme) { + if ([identifier isEqualToString:QDThemeIdentifierDark]) { + return UIColorForBackground; + } + return UIColorMake(246, 246, 246); + }]; self.toolbarView.qmui_borderColor = UIColorSeparator; self.toolbarView.qmui_borderPosition = QMUIViewBorderPositionTop; [self.containerView addSubview:self.toolbarView]; @@ -87,8 +94,7 @@ - (void)viewDidLayoutSubviews { self.maskControl.frame = self.view.bounds; - CGRect containerRect = CGRectFlatMake(0, CGRectGetHeight(self.view.bounds), CGRectGetWidth(self.view.bounds), 300); - self.containerView.frame = CGRectApplyAffineTransformWithAnchorPoint(containerRect, self.containerView.transform, self.containerView.layer.anchorPoint); + self.containerView.qmui_frameApplyTransform = CGRectFlatMake(0, CGRectGetHeight(self.view.bounds), CGRectGetWidth(self.view.bounds), 300); self.toolbarView.frame = CGRectFlatMake(0, CGRectGetHeight(self.containerView.bounds) - kToolbarHeight, CGRectGetWidth(self.containerView.bounds), kToolbarHeight); self.cancelButton.frame = CGRectFlatMake(20, CGFloatGetCenter(CGRectGetHeight(self.toolbarView.bounds), CGRectGetHeight(self.cancelButton.bounds)), CGRectGetWidth(self.cancelButton.bounds), CGRectGetHeight(self.cancelButton.bounds)); @@ -112,12 +118,12 @@ - (void)removeFromParentViewController { [super removeFromParentViewController]; } +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + return UIInterfaceOrientationMaskPortrait; +} + - (void)showInParentViewController:(UIViewController *)controller { - if (IS_LANDSCAPE) { - [QDUIHelper forceInterfaceOrientationPortrait]; - } - // 这一句访问了self.view,触发viewDidLoad: self.view.frame = controller.view.bounds; @@ -149,16 +155,27 @@ - (void)hide { [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ self.maskControl.alpha = 0.0; } completion:^(BOOL finished) { + UIViewController *parentViewController = self.parentViewController; [self.view removeFromSuperview]; [self removeFromParentViewController]; // 这一句触发viewDidDisappear: [self endAppearanceTransition]; [self.view removeFromSuperview]; + + // 如有必要,旋转屏幕 + [parentViewController qmui_setNeedsUpdateOfSupportedInterfaceOrientations]; }]; } - (void)handleCancelButtonEvent:(id)sender { - [self.textView resignFirstResponder]; + if (self.textView.isFirstResponder) { + [self.textView resignFirstResponder]; + } else { + [UIView animateWithDuration:.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + self.containerView.layer.transform = CATransform3DIdentity; + } completion:nil]; + [self hide]; + } } #pragma mark - @@ -167,13 +184,13 @@ - (void)keyboardWillChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUser __weak __typeof(self)weakSelf = self; [QMUIKeyboardManager handleKeyboardNotificationWithUserInfo:keyboardUserInfo showBlock:^(QMUIKeyboardUserInfo *keyboardUserInfo) { [QMUIKeyboardManager animateWithAnimated:YES keyboardUserInfo:keyboardUserInfo animations:^{ - CGFloat distanceFromBottom = [QMUIKeyboardManager distanceFromMinYToBottomInView:weakSelf.view keyboardRect:keyboardUserInfo.endFrame]; - weakSelf.containerView.layer.transform = CATransform3DMakeTranslation(0, - distanceFromBottom - CGRectGetHeight(self.containerView.bounds), 0); + CGFloat distanceFromBottom = keyboardUserInfo.isFloatingKeyboard ? 0 : [QMUIKeyboardManager distanceFromMinYToBottomInView:weakSelf.view keyboardRect:keyboardUserInfo.endFrame]; + weakSelf.containerView.transform = CGAffineTransformMakeTranslation(0, - distanceFromBottom - CGRectGetHeight(weakSelf.containerView.bounds)); } completion:NULL]; } hideBlock:^(QMUIKeyboardUserInfo *keyboardUserInfo) { [weakSelf hide]; [QMUIKeyboardManager animateWithAnimated:YES keyboardUserInfo:keyboardUserInfo animations:^{ - weakSelf.containerView.layer.transform = CATransform3DIdentity; + weakSelf.containerView.transform = CGAffineTransformIdentity; } completion:NULL]; }]; } @@ -211,7 +228,7 @@ - (void)initSubviews { _contentLabel = [[QMUILabel alloc] init]; self.contentLabel.numberOfLines = 0; - NSMutableAttributedString *contentAttributedString = [[NSMutableAttributedString alloc] initWithString:@"QMUIKeyboardManager 以更方便的方式管理键盘事件,无需再关心 notification、键盘坐标转换、判断是否目标输入框等问题,并兼容 iPad 浮动键盘和外接键盘。\nQMUIKeyboardManager 有两种使用方式,一种是直接使用,一种是集成到 UITextField(QMUI) 及 UITextView(QMUI) 内。" attributes:@{NSFontAttributeName:UIFontMake(16),NSForegroundColorAttributeName:UIColorGray1,NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:24 lineBreakMode:NSLineBreakByCharWrapping]}]; + NSMutableAttributedString *contentAttributedString = [[NSMutableAttributedString alloc] initWithString:@"QMUIKeyboardManager 以更方便的方式管理键盘事件,无需再关心 notification、键盘坐标转换、判断是否目标输入框等问题,并兼容 iPad 浮动键盘和外接键盘。\nQMUIKeyboardManager 有两种使用方式,一种是直接使用,一种是集成到 UITextField(QMUI) 及 UITextView(QMUI) 内。" attributes:@{NSFontAttributeName:UIFontMake(16),NSForegroundColorAttributeName:UIColor.qd_mainTextColor,NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:24 lineBreakMode:NSLineBreakByCharWrapping]}]; NSDictionary *codeAttributes = CodeAttributes(16); [contentAttributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { if (![codeString isEqualToString:@"notification"] && ![codeString isEqualToString:@"iPad"]) { @@ -234,7 +251,7 @@ - (void)initSubviews { [self.view addSubview:self.writeReviewButton]; _toolbarView = [[UIView alloc] init]; - self.toolbarView.backgroundColor = UIColorWhite; + self.toolbarView.backgroundColor = UIColorForBackground; self.toolbarView.qmui_borderColor = UIColorSeparator; self.toolbarView.qmui_borderPosition = QMUIViewBorderPositionTop; [self.view addSubview:self.toolbarView]; @@ -243,7 +260,7 @@ - (void)initSubviews { self.toolbarTextField.delegate = self; self.toolbarTextField.placeholder = @"发表评论..."; self.toolbarTextField.font = UIFontMake(15); - self.toolbarTextField.backgroundColor = UIColorWhite; + self.toolbarTextField.backgroundColor = nil; [self.toolbarView addSubview:self.toolbarTextField]; __weak __typeof(self)weakSelf = self; @@ -262,7 +279,7 @@ - (void)initSubviews { _faceButton = [[QMUIButton alloc] init]; self.faceButton.titleLabel.font = UIFontMake(16); self.faceButton.qmui_outsideEdge = UIEdgeInsetsMake(-12, -12, -12, -12); - [self.faceButton setImage:[UIImageMake(@"icon_emotion") qmui_imageWithTintColor:UIColorGray5] forState:UIControlStateNormal]; + [self.faceButton setImage:[UIImageMake(@"icon_emotion") qmui_imageWithTintColor:UIColor.qd_descriptionTextColor] forState:UIControlStateNormal]; [self.faceButton setImage:UIImageMake(@"icon_emotion") forState:UIControlStateSelected]; [self.faceButton sizeToFit]; [self.faceButton addTarget:self action:@selector(handleFaceButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; @@ -300,8 +317,7 @@ - (BOOL)shouldAutomaticallyForwardAppearanceMethods { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - CGRect toolbarRect = CGRectFlatMake(0, CGRectGetHeight(self.view.bounds), CGRectGetWidth(self.view.bounds), kToolbarHeight); - self.toolbarView.frame = CGRectApplyAffineTransformWithAnchorPoint(toolbarRect, self.toolbarView.transform, self.toolbarView.layer.anchorPoint); + self.toolbarView.qmui_frameApplyTransform = CGRectFlatMake(0, CGRectGetHeight(self.view.bounds), CGRectGetWidth(self.view.bounds), kToolbarHeight); CGFloat textFieldInset = 8; CGFloat textFieldHeight = kToolbarHeight - textFieldInset * 2; @@ -418,7 +434,7 @@ - (void)showToolbarViewWithKeyboardUserInfo:(QMUIKeyboardUserInfo *)keyboardUser if (keyboardUserInfo) { // 相对于键盘 [QMUIKeyboardManager animateWithAnimated:YES keyboardUserInfo:keyboardUserInfo animations:^{ - CGFloat distanceFromBottom = [QMUIKeyboardManager distanceFromMinYToBottomInView:self.view keyboardRect:keyboardUserInfo.endFrame]; + CGFloat distanceFromBottom = keyboardUserInfo.isFloatingKeyboard ? 0 : [QMUIKeyboardManager distanceFromMinYToBottomInView:self.view keyboardRect:keyboardUserInfo.endFrame]; self.toolbarView.layer.transform = CATransform3DMakeTranslation(0, - distanceFromBottom - kToolbarHeight, 0); self.emotionInputManager.emotionView.layer.transform = CATransform3DMakeTranslation(0, - distanceFromBottom, 0); } completion:NULL]; diff --git a/qmuidemo/Modules/Demos/Components/QDLayouterViewController.h b/qmuidemo/Modules/Demos/Components/QDLayouterViewController.h new file mode 100644 index 00000000..9092ff07 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDLayouterViewController.h @@ -0,0 +1,17 @@ +// +// QDLayouterViewController.h +// qmuidemo +// +// Created by molice on 2024/1/4. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDLayouterViewController : QDCommonViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Components/QDLayouterViewController.m b/qmuidemo/Modules/Demos/Components/QDLayouterViewController.m new file mode 100644 index 00000000..c326b2e7 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDLayouterViewController.m @@ -0,0 +1,163 @@ +// +// QDLayouterViewController.m +// qmuidemo +// +// Created by molice on 2024/1/4. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import "QDLayouterViewController.h" +#import "QMUIInteractiveDebugger.h" + +@interface QDLayouterView : UIControl +@property(nonatomic, strong) QMUILayouterItem *item; +@end + +@implementation QDLayouterView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.backgroundColor = [QDCommonUI.randomThemeColor colorWithAlphaComponent:.5]; + self.qmui_sizeThatFitsBlock = ^CGSize(__kindof UIView * _Nonnull view, CGSize size, CGSize superResult) { + return CGSizeMake(48, 48); + }; + } + return self; +} + +- (QMUILayouterItem *)item { + if (!_item) { + _item = [QMUILayouterItem itemWithView:self margin:UIEdgeInsetsMake(8, 8, 8, 8)]; + } + return _item; +} + +- (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; + self.alpha = highlighted ? UIControlHighlightedAlpha : 1; +} + +@end + +@interface QDLayouterViewController () +@property(nonatomic, strong) NSMutableArray *horizontalViews; +@property(nonatomic, strong) QMUIInteractiveDebugPanelViewController *vc; +@end + +@implementation QDLayouterViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.horizontalViews = NSMutableArray.new; + for (NSInteger i = 0; i < 3; i++) { + QDLayouterView *view = [[QDLayouterView alloc] init]; + [view addTarget:self action:@selector(handleViewEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:view]; + [self.horizontalViews addObject:view]; + } +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + QMUILayouterLinearHorizontal *h = [QMUILayouterLinearHorizontal itemWithChildItems:[self.horizontalViews qmui_mapWithBlock:^id _Nonnull(QDLayouterView * _Nonnull view, NSInteger index) { + return view.item; + }] spacingBetweenItems:0]; + [h showDebugBorderRecursivelyInView:self.view]; + h.frame = CGRectMake(32 + self.view.safeAreaInsets.left, 32 + self.qmui_navigationBarMaxYInViewCoordinator, self.view.qmui_width - UIEdgeInsetsGetHorizontalValue(self.view.safeAreaInsets) - 32 * 2, QMUIViewSelfSizingHeight); +} + +- (void)itemDidChange { + [self.view setNeedsLayout]; +} + +- (void)handleViewEvent:(QDLayouterView *)view { + __weak __typeof(self)weakSelf = self; + QMUILayouterItem *item = view.item; + if (self.vc) { + [self.vc.view removeFromSuperview]; + self.vc = nil; + } + QMUIInteractiveDebugPanelViewController *vc = [QDUIHelper generateDebugViewControllerWithTitle:@"配置 Item" items:@[ + [QMUIInteractiveDebugPanelItem sliderItemWithTitle:@"width" minValue:0 maxValue:201.0 valueGetter:^(UISlider * _Nonnull actionView) { + actionView.value = view.qmui_width; + } valueSetter:^(UISlider * _Nonnull actionView) { + CGFloat v = actionView.value > 200 ? 99999 : actionView.value; + CGSize s = view.qmui_sizeThatFitsBlock(view, CGSizeZero, CGSizeZero); + view.qmui_sizeThatFitsBlock = ^CGSize(__kindof UIView * _Nonnull view, CGSize size, CGSize superResult) { + return CGSizeMake(ceil(v), s.height); + }; + [weakSelf itemDidChange]; + }], + [QMUIInteractiveDebugPanelItem sliderItemWithTitle:@"height" minValue:0 maxValue:201.0 valueGetter:^(UISlider * _Nonnull actionView) { + actionView.value = view.qmui_height; + } valueSetter:^(UISlider * _Nonnull actionView) { + CGFloat v = actionView.value > 200 ? 99999 : actionView.value; + CGSize s = view.qmui_sizeThatFitsBlock(view, CGSizeZero, CGSizeZero); + view.qmui_sizeThatFitsBlock = ^CGSize(__kindof UIView * _Nonnull view, CGSize size, CGSize superResult) { + return CGSizeMake(s.width, ceil(v)); + }; + [weakSelf itemDidChange]; + }], + [QMUIInteractiveDebugPanelItem sliderItemWithTitle:@"minimumWidth" minValue:0 maxValue:200 valueGetter:^(UISlider * _Nonnull actionView) { + actionView.value = item.minimumSize.width; + } valueSetter:^(UISlider * _Nonnull actionView) { + item.minimumSize = CGSizeMake(ceil(actionView.value), item.minimumSize.height); + [weakSelf itemDidChange]; + }], + [QMUIInteractiveDebugPanelItem sliderItemWithTitle:@"maximumWidth" minValue:0 maxValue:201.0 valueGetter:^(UISlider * _Nonnull actionView) { + actionView.value = item.maximumSize.width; + } valueSetter:^(UISlider * _Nonnull actionView) { + CGFloat v = actionView.value > 200 ? CGFLOAT_MAX : actionView.value; + item.maximumSize = CGSizeMake(ceil(v), item.maximumSize.height); + [weakSelf itemDidChange]; + }], + [QMUIInteractiveDebugPanelItem sliderItemWithTitle:@"minimumHeight" minValue:0 maxValue:200 valueGetter:^(UISlider * _Nonnull actionView) { + actionView.value = item.minimumSize.height; + } valueSetter:^(UISlider * _Nonnull actionView) { + item.minimumSize = CGSizeMake(item.minimumSize.width, ceil(actionView.value)); + [weakSelf itemDidChange]; + }], + [QMUIInteractiveDebugPanelItem sliderItemWithTitle:@"maximumHeight" minValue:0 maxValue:201.0 valueGetter:^(UISlider * _Nonnull actionView) { + actionView.value = item.maximumSize.height; + } valueSetter:^(UISlider * _Nonnull actionView) { + CGFloat v = actionView.value > 200 ? CGFLOAT_MAX : actionView.value; + item.maximumSize = CGSizeMake(item.maximumSize.width, ceil(v)); + [weakSelf itemDidChange]; + }], + [QMUIInteractiveDebugPanelItem sliderItemWithTitle:@"margin" minValue:0 maxValue:40 valueGetter:^(UISlider * _Nonnull actionView) { + actionView.value = item.margin.left; + } valueSetter:^(UISlider * _Nonnull actionView) { + CGFloat v = ceil(actionView.value); + item.margin = UIEdgeInsetsMake(v, v, v, v); + [weakSelf itemDidChange]; + }], + [QMUIInteractiveDebugPanelItem sliderItemWithTitle:@"grow" minValue:0 maxValue:100 valueGetter:^(UISlider * _Nonnull actionView) { + actionView.value = item.grow; + } valueSetter:^(UISlider * _Nonnull actionView) { + item.grow = actionView.value; + [weakSelf itemDidChange]; + }], + [QMUIInteractiveDebugPanelItem sliderItemWithTitle:@"shrink" minValue:0 maxValue:100 valueGetter:^(UISlider * _Nonnull actionView) { + actionView.value = item.shrink; + } valueSetter:^(UISlider * _Nonnull actionView) { + item.shrink = actionView.value; + [weakSelf itemDidChange]; + }], + ]]; + self.vc = vc; + vc.view.layer.borderWidth = 0; + CGSize size = [vc contentSizeThatFits:CGSizeMake(300, CGFLOAT_MAX)]; + vc.view.frame = CGRectMakeWithSize(CGSizeMake(300, size.height)); + QMUIPopupContainerView *popup = [[QMUIPopupContainerView alloc] init]; + popup.automaticallyHidesWhenUserTap = YES; + popup.contentEdgeInsets = UIEdgeInsetsZero; + [popup.contentView addSubview:vc.view]; + popup.contentViewSizeThatFitsBlock = ^CGSize(CGSize aSize) { + return size; + }; + popup.sourceView = view; + [popup showWithAnimated:YES]; +} + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDMarqueeLabelViewController.m b/qmuidemo/Modules/Demos/Components/QDMarqueeLabelViewController.m index 6a505083..0da9eef4 100644 --- a/qmuidemo/Modules/Demos/Components/QDMarqueeLabelViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDMarqueeLabelViewController.m @@ -14,6 +14,7 @@ @interface QDMarqueeLabelViewController () *labels = [self.view.subviews qmui_filterWithBlock:^BOOL(__kindof UIView * _Nonnull item) { + return [item isKindOfClass:UILabel.class]; + }]; + [labels enumerateObjectsUsingBlock:^(UILabel * _Nonnull label, NSUInteger idx, BOOL * _Nonnull stop) { + label.frame = CGRectMake(labelMinX, minY, labelWidth, QMUIViewSelfSizingHeight); minY = CGRectGetMaxY(label.frame) + labelSpacing; - } + }]; self.collectionView.frame = CGRectMake(0, CGRectGetMaxY(self.separatorLayer.frame), CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds) - CGRectGetMaxY(self.separatorLayer.frame)); self.collectionViewLayout.itemSize = CGSizeMake(CGRectGetWidth(self.collectionView.bounds) - UIEdgeInsetsGetHorizontalValue(self.collectionViewLayout.sectionInset), CGRectGetHeight(self.collectionView.bounds) - UIEdgeInsetsGetVerticalValue(self.collectionViewLayout.sectionInset)); @@ -103,17 +112,20 @@ - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSe - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { QDMarqueeCollectionViewCell *cell = (QDMarqueeCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath]; cell.label.text = @"在可复用的 UIView 里使用 QMUIMarqueeLabel 时,需要手动触发动画、停止动画,否则可能在滚动过程中动画会不正确地被开启/关闭"; + cell.label2.text = @"普通 UILabel 也可以开启 marquee 效果,性能比 QMUIMarqueeLabel 更好,但功能没那么丰富。"; return cell; } -- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { +- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(QDMarqueeCollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { // 在 willDisplayCell 里开启动画(不能在 cellForItem 里开启,是因为 cellForItem 的时候,cell 尚未被 add 到 collectionView 上,cell.window 为 nil) - [((QDMarqueeCollectionViewCell *)cell).label requestToStartAnimation]; + [cell.label requestToStartAnimation]; + [cell.label2 qmui_startNativeMarquee]; } -- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(QDMarqueeCollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { // 在 didEndDisplayingCell 里停止动画,避免资源消耗 - [((QDMarqueeCollectionViewCell *)cell).label requestToStopAnimation]; + [cell.label requestToStopAnimation]; + [cell.label2 qmui_stopNativeMarquee]; } @end @@ -128,13 +140,18 @@ - (instancetype)initWithFrame:(CGRect)frame { self.label = [[QMUIMarqueeLabel alloc] qmui_initWithFont:UIFontMake(16) textColor:UIColorWhite]; [self.label qmui_calculateHeightAfterSetAppearance]; [self.contentView addSubview:self.label]; + + self.label2 = [[UILabel alloc] qmui_initWithFont:self.label.font textColor:self.label.textColor]; + [self.label2 qmui_startNativeMarquee]; + [self.contentView addSubview:self.label2]; } return self; } - (void)layoutSubviews { [super layoutSubviews]; - self.label.frame = CGRectMake(24, CGFloatGetCenter(CGRectGetHeight(self.contentView.bounds), CGRectGetHeight(self.label.frame)), CGRectGetWidth(self.contentView.bounds) - 24 * 2, CGRectGetHeight(self.label.frame)); + self.label.frame = CGRectMake(24, CGFloatGetCenter(CGRectGetHeight(self.contentView.bounds), CGRectGetHeight(self.label.frame)) - 10, CGRectGetWidth(self.contentView.bounds) - 24 * 2, QMUIViewSelfSizingHeight); + self.label2.frame = CGRectSetY(self.label.frame, CGRectGetMaxY(self.label.frame) + 10); } @end diff --git a/qmuidemo/Modules/Demos/Components/QDModalPresentationViewController.m b/qmuidemo/Modules/Demos/Components/QDModalPresentationViewController.m index 16a56918..40cd7f50 100644 --- a/qmuidemo/Modules/Demos/Components/QDModalPresentationViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDModalPresentationViewController.m @@ -72,14 +72,14 @@ - (void)didSelectCellWithTitle:(NSString *)title { - (void)handleShowContentView { UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 400)]; - contentView.backgroundColor = UIColorWhite; + contentView.backgroundColor = UIColor.qd_backgroundColorLighten; contentView.layer.cornerRadius = 6; UILabel *label = [[UILabel alloc] init]; label.numberOfLines = 0; NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:24]; paragraphStyle.paragraphSpacing = 16; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"默认的布局是上下左右居中,可通过contentViewMargins、maximumContentViewWidth属性来调整宽高、上下左右的偏移。\n你现在可以试试旋转一下设备试试看。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColorBlack, NSParagraphStyleAttributeName: paragraphStyle}]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"默认的布局是上下左右居中,可通过contentViewMargins、maximumContentViewWidth属性来调整宽高、上下左右的偏移。\n你现在可以试试旋转一下设备试试看。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColor.qd_mainTextColor, NSParagraphStyleAttributeName: paragraphStyle}]; NSDictionary *codeAttributes = CodeAttributes(16); [attributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { [attributedString addAttributes:codeAttributes range:codeRange]; @@ -105,14 +105,14 @@ - (void)handleShowContentViewController { - (void)handleAnimationStyle { UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 400)]; - contentView.backgroundColor = UIColorWhite; + contentView.backgroundColor = UIColor.qd_backgroundColorLighten; contentView.layer.cornerRadius = 6; UILabel *label = [[UILabel alloc] init]; label.numberOfLines = 0; NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:24]; paragraphStyle.paragraphSpacing = 16; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"modalViewController提供的显示/隐藏动画总共有3种,可通过animationStyle属性来设置,默认为QMUIModalPresentationAnimationStyleFade。\n多次打开此浮层会在这3种动画之间互相切换。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColorBlack, NSParagraphStyleAttributeName: paragraphStyle}]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"modalViewController提供的显示/隐藏动画总共有3种,可通过animationStyle属性来设置,默认为QMUIModalPresentationAnimationStyleFade。\n多次打开此浮层会在这3种动画之间互相切换。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColor.qd_mainTextColor, NSParagraphStyleAttributeName: paragraphStyle}]; NSDictionary *codeAttributes = CodeAttributes(16); [attributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { @@ -135,9 +135,11 @@ - (void)handleAnimationStyle { - (void)handleCustomDimmingView { UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 400)]; - contentView.backgroundColor = UIColorWhite; + contentView.backgroundColor = UIColor.qd_backgroundColorLighten; contentView.layer.cornerRadius = 6; - contentView.layer.shadowColor = UIColorBlack.CGColor; + contentView.layer.shadowColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return [identifier isEqualToString:QDThemeIdentifierDark] ? UIColorWhite : UIColorBlack; + }].CGColor; contentView.layer.shadowOpacity = .08; contentView.layer.shadowRadius = 15; contentView.layer.shadowPath = [UIBezierPath bezierPathWithRoundedRect:contentView.bounds cornerRadius:contentView.layer.cornerRadius].CGPath; @@ -146,7 +148,7 @@ - (void)handleCustomDimmingView { label.numberOfLines = 0; NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:24]; paragraphStyle.paragraphSpacing = 16; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"QMUIModalPresentationViewController允许自定义背景遮罩的dimmingView,例如这里的背景遮罩是拿当前界面进行截图磨砂后显示出来的。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColorBlack, NSParagraphStyleAttributeName: paragraphStyle}]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"QMUIModalPresentationViewController允许自定义背景遮罩的dimmingView,例如这里的背景遮罩是拿当前界面进行截图磨砂后显示出来的。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColor.qd_mainTextColor, NSParagraphStyleAttributeName: paragraphStyle}]; NSDictionary *codeAttributes = CodeAttributes(16); [attributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { @@ -160,7 +162,11 @@ - (void)handleCustomDimmingView { label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding), QMUIViewSelfSizingHeight); UIImage *blurredBackgroundImage = [UIImage qmui_imageWithView:self.navigationController.view]; - blurredBackgroundImage = [UIImageEffects imageByApplyingExtraLightEffectToImage:blurredBackgroundImage]; + if ([QMUIThemeManagerCenter.defaultThemeManager.currentThemeIdentifier isEqual:QDThemeIdentifierDark]) { + blurredBackgroundImage = [UIImageEffects imageByApplyingDarkEffectToImage:blurredBackgroundImage]; + } else { + blurredBackgroundImage = [UIImageEffects imageByApplyingExtraLightEffectToImage:blurredBackgroundImage]; + } UIImageView *blurredDimmingView = [[UIImageView alloc] initWithImage:blurredBackgroundImage]; QMUIModalPresentationViewController *modalViewController = [[QMUIModalPresentationViewController alloc] init]; @@ -171,7 +177,7 @@ - (void)handleCustomDimmingView { - (void)handleLayoutBlockAndAnimation { UIScrollView *contentView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 300, 250)]; - contentView.backgroundColor = UIColorWhite; + contentView.backgroundColor = UIColor.qd_backgroundColorLighten; contentView.layer.cornerRadius = 6; contentView.alwaysBounceVertical = NO; @@ -179,7 +185,7 @@ - (void)handleLayoutBlockAndAnimation { label.numberOfLines = 0; NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:24]; paragraphStyle.paragraphSpacing = 16; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"利用layoutBlock可以自定义浮层的布局,注意此时contentViewMargins、maximumContentViewWidth属性均无效,如果需要实现外间距、最大宽高的保护,请自行计算。\n另外搭配showingAnimation、hidingAnimation也可制作自己的显示/隐藏动画,例如这个例子里实现了一个从底部升起的面板,升起后停靠在容器底端,你可以试着旋转设备,会发现依然能正确布局。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColorBlack, NSParagraphStyleAttributeName: paragraphStyle}]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"利用layoutBlock可以自定义浮层的布局,注意此时contentViewMargins、maximumContentViewWidth属性均无效,如果需要实现外间距、最大宽高的保护,请自行计算。\n另外搭配showingAnimation、hidingAnimation也可制作自己的显示/隐藏动画,例如这个例子里实现了一个从底部升起的面板,升起后停靠在容器底端,你可以试着旋转设备,会发现依然能正确布局。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColor.qd_mainTextColor, NSParagraphStyleAttributeName: paragraphStyle}]; NSDictionary *codeAttributes = CodeAttributes(16); [attributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { @@ -195,40 +201,19 @@ - (void)handleLayoutBlockAndAnimation { contentView.contentSize = CGSizeMake(CGRectGetWidth(contentView.bounds), CGRectGetMaxY(label.frame) + contentViewPadding.bottom); QMUIModalPresentationViewController *modalViewController = [[QMUIModalPresentationViewController alloc] init]; + modalViewController.animationStyle = QMUIModalPresentationAnimationStyleSlide; modalViewController.contentView = contentView; modalViewController.layoutBlock = ^(CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewDefaultFrame) { - contentView.frame = CGRectSetXY(contentView.frame, CGFloatGetCenter(CGRectGetWidth(containerBounds), CGRectGetWidth(contentView.frame)), CGRectGetHeight(containerBounds) - 20 - CGRectGetHeight(contentView.frame)); - }; - modalViewController.showingAnimation = ^(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewFrame, void(^completion)(BOOL finished)) { - contentView.frame = CGRectSetY(contentView.frame, CGRectGetHeight(containerBounds)); - dimmingView.alpha = 0; - [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - dimmingView.alpha = 1; - contentView.frame = contentViewFrame; - } completion:^(BOOL finished) { - // 记住一定要在适当的时机调用completion() - if (completion) { - completion(finished); - } - }]; - }; - modalViewController.hidingAnimation = ^(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, void(^completion)(BOOL finished)) { - [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - dimmingView.alpha = 0.0; - contentView.frame = CGRectSetY(contentView.frame, CGRectGetHeight(containerBounds)); - } completion:^(BOOL finished) { - // 记住一定要在适当的时机调用completion() - if (completion) { - completion(finished); - } - }]; + contentView.qmui_frameApplyTransform = CGRectSetXY(contentView.frame, CGFloatGetCenter(CGRectGetWidth(containerBounds), CGRectGetWidth(contentView.frame)), CGRectGetHeight(containerBounds) - 20 - CGRectGetHeight(contentView.frame)); }; [modalViewController showWithAnimated:YES completion:nil]; } - (void)handleKeyboard { - UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 200)]; - contentView.backgroundColor = UIColorWhite; + UIScrollView *contentView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 300, 200)]; + contentView.alwaysBounceVertical = NO; + contentView.alwaysBounceHorizontal = NO; + contentView.backgroundColor = UIColor.qd_backgroundColorLighten; contentView.layer.cornerRadius = 6; UIEdgeInsets contentViewPadding = UIEdgeInsetsMake(20, 20, 20, 20); @@ -245,9 +230,9 @@ - (void)handleKeyboard { label.numberOfLines = 0; NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]; paragraphStyle.paragraphSpacing = 10; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"如果你的浮层里有输入框,建议在把输入框添加到界面上后立即调用becomeFirstResponder(如果你用contentViewController,则在viewWillAppear:时调用becomeFirstResponder),以保证键盘跟随浮层一起显示。\n而在浮层消失时,modalViewController会自动降下键盘,所以你的浮层里并不需要处理。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColorGrayDarken, NSParagraphStyleAttributeName: paragraphStyle}]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"如果你的浮层里有输入框,建议在把输入框添加到界面上后立即调用becomeFirstResponder(如果你用contentViewController,则在viewWillAppear:时调用becomeFirstResponder),以保证键盘跟随浮层一起显示。\n而在浮层消失时,modalViewController会自动降下键盘,所以你的浮层里并不需要处理。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColor.qd_mainTextColor, NSParagraphStyleAttributeName: paragraphStyle}]; - NSDictionary *codeAttributes = @{NSFontAttributeName: CodeFontMake(12), NSForegroundColorAttributeName: [[QDThemeManager sharedInstance].currentTheme.themeCodeColor colorWithAlphaComponent:.8]}; + NSDictionary *codeAttributes = @{NSFontAttributeName: CodeFontMake(12), NSForegroundColorAttributeName: UIColor.qd_codeColor}; [attributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { [attributedString addAttributes:codeAttributes range:codeRange]; }]; @@ -256,25 +241,35 @@ - (void)handleKeyboard { [contentView addSubview:label]; label.frame = CGRectMake(contentViewPadding.left, CGRectGetMaxY(textField.frame) + 8, contentLimitWidth, QMUIViewSelfSizingHeight); - - contentView.frame = CGRectSetHeight(contentView.frame, CGRectGetMaxY(label.frame) + contentViewPadding.bottom); + contentView.contentSize = CGSizeMake(CGRectGetWidth(contentView.bounds), CGRectGetMaxY(label.frame) + contentViewPadding.bottom); QMUIModalPresentationViewController *modalViewController = [[QMUIModalPresentationViewController alloc] init]; modalViewController.animationStyle = QMUIModalPresentationAnimationStyleSlide; + __weak __typeof(modalViewController)weakModal = modalViewController; + modalViewController.layoutBlock = ^(CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewDefaultFrame) { + + CGSize contentViewContainerSize = CGSizeMake(CGRectGetWidth(containerBounds) - UIEdgeInsetsGetHorizontalValue(weakModal.contentViewMargins), CGRectGetHeight(containerBounds) - keyboardHeight - UIEdgeInsetsGetVerticalValue(weakModal.contentViewMargins)); + CGSize contentViewLimitSize = CGSizeMake(MIN(weakModal.maximumContentViewWidth, contentViewContainerSize.width), contentViewContainerSize.height); + CGSize contentViewSize = contentView.contentSize; + contentViewSize.width = MIN(contentViewLimitSize.width, contentViewSize.width); + contentViewSize.height = MIN(contentViewLimitSize.height, contentViewSize.height); + CGRect contentViewFrame = CGRectMake(CGFloatGetCenter(contentViewContainerSize.width, contentViewSize.width) + weakModal.contentViewMargins.left, CGFloatGetCenter(contentViewContainerSize.height, contentViewSize.height) + weakModal.contentViewMargins.top, contentViewSize.width, contentViewSize.height); + contentView.qmui_frameApplyTransform = contentViewFrame; + }; modalViewController.contentView = contentView; [modalViewController showWithAnimated:YES completion:nil]; } - (void)handleWindowShowing { - UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 160)]; - contentView.backgroundColor = UIColorWhite; + UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 208)]; + contentView.backgroundColor = UIColor.qd_backgroundColorLighten; contentView.layer.cornerRadius = 6; UILabel *label = [[UILabel alloc] init]; label.numberOfLines = 0; NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:24]; paragraphStyle.paragraphSpacing = 16; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"QMUIModalPresentationViewController支持 3 种使用方式,当前使用第 1 种,注意状态栏被遮罩盖住了" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColorBlack, NSParagraphStyleAttributeName: paragraphStyle}]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"QMUIModalPresentationViewController支持 3 种使用方式,当前使用第 1 种。提供 UIWindow 的方式目的是为了盖住状态栏,但 iOS 13 及以后,状态栏已经无法被盖住了,所以只有在 iOS 12 及以下才能看到状态栏盖住的效果。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColor.qd_mainTextColor, NSParagraphStyleAttributeName: paragraphStyle}]; NSDictionary *codeAttributes = CodeAttributes(16); [attributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { [attributedString addAttributes:codeAttributes range:codeRange]; @@ -293,14 +288,14 @@ - (void)handleWindowShowing { - (void)handlePresentShowing { UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 160)]; - contentView.backgroundColor = UIColorWhite; + contentView.backgroundColor = UIColor.qd_backgroundColorLighten; contentView.layer.cornerRadius = 6; UILabel *label = [[UILabel alloc] init]; label.numberOfLines = 0; NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:24]; paragraphStyle.paragraphSpacing = 16; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"QMUIModalPresentationViewController支持 3 种使用方式,当前使用第 2 种,注意遮罩无法盖住屏幕顶部的状态栏。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColorBlack, NSParagraphStyleAttributeName: paragraphStyle}]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"QMUIModalPresentationViewController支持 3 种使用方式,当前使用第 2 种,注意遮罩无法盖住屏幕顶部的状态栏。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColor.qd_mainTextColor, NSParagraphStyleAttributeName: paragraphStyle}]; NSDictionary *codeAttributes = CodeAttributes(16); [attributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { [attributedString addAttributes:codeAttributes range:codeRange]; @@ -318,36 +313,49 @@ - (void)handlePresentShowing { } - (void)handleShowInView { - if (self.modalViewControllerForAddSubview) { - [self.modalViewControllerForAddSubview hideInView:self.view animated:YES completion:nil]; + if (!self.modalViewControllerForAddSubview) { + CGRect modalRect = CGRectMake(40, self.qmui_navigationBarMaxYInViewCoordinator + 40, CGRectGetWidth(self.view.bounds) - 40 * 2, CGRectGetHeight(self.view.bounds) - self.qmui_navigationBarMaxYInViewCoordinator - 40 * 2); + + UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(modalRect) - 40, 0)]; + contentView.backgroundColor = UIColor.qd_backgroundColorLighten; + contentView.layer.cornerRadius = 6; + + UILabel *label = [[UILabel alloc] init]; + label.numberOfLines = 0; + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:24]; + paragraphStyle.paragraphSpacing = 16; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"QMUIModalPresentationViewController支持 3 种使用方式,当前使用第 3 种,注意可以透过遮罩外的空白地方点击到背后的 cell。\n这种方式适用于需要保持浮层显示的情况下切换界面,你可以点击按钮看效果。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColor.qd_mainTextColor, NSParagraphStyleAttributeName: paragraphStyle}]; + NSDictionary *codeAttributes = CodeAttributes(16); + [attributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { + [attributedString addAttributes:codeAttributes range:codeRange]; + }]; + label.attributedText = attributedString; + [contentView addSubview:label]; + + QMUIButton *button = [[QMUIButton alloc] init]; + button.tintColorAdjustsTitleAndImage = ButtonTintColor; + button.titleLabel.font = UIFontMake(16); + [button setTitle:@"进入下一个界面" forState:UIControlStateNormal]; + [button setImage:TableViewCellDisclosureIndicatorImage forState:UIControlStateNormal]; + button.spacingBetweenImageAndTitle = 4; + button.imagePosition = QMUIButtonImagePositionRight; + [button sizeToFit]; + button.qmui_tapBlock = ^(__kindof UIControl *sender) { + [QMUIHelper.visibleViewController.navigationController pushViewController:QDModalPresentationViewController.new animated:YES]; + }; + [contentView addSubview:button]; + + UIEdgeInsets contentViewPadding = UIEdgeInsetsMake(20, 20, 20, 20); + label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding), QMUIViewSelfSizingHeight); + button.frame = CGRectSetXY(button.frame, CGRectGetMinX(label.frame), CGRectGetMaxY(label.frame) + 24); + contentView.frame = CGRectSetHeight(contentView.frame, CGRectGetMaxY(button.frame) + contentViewPadding.bottom); + + self.modalViewControllerForAddSubview = [[QMUIModalPresentationViewController alloc] init]; + self.modalViewControllerForAddSubview.contentView = contentView; + self.modalViewControllerForAddSubview.view.frame = modalRect;// 为了展示,故意让浮层小于当前界面,以展示局部浮层的能力 + // 以 addSubview 的形式显示,此时需要retain住modalPresentationViewController,防止提前被释放 } - - CGRect modalRect = CGRectMake(40, self.qmui_navigationBarMaxYInViewCoordinator + 40, CGRectGetWidth(self.view.bounds) - 40 * 2, CGRectGetHeight(self.view.bounds) - self.qmui_navigationBarMaxYInViewCoordinator - 40 * 2); - - UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(modalRect) - 40, 200)]; - contentView.backgroundColor = UIColorWhite; - contentView.layer.cornerRadius = 6; - - self.modalViewControllerForAddSubview = [[QMUIModalPresentationViewController alloc] init]; - self.modalViewControllerForAddSubview.contentView = contentView; - self.modalViewControllerForAddSubview.view.frame = modalRect; - // 以 addSubview 的形式显示,此时需要retain住modalPresentationViewController,防止提前被释放 - [self.modalViewControllerForAddSubview showInView:self.view animated:YES completion:nil]; - - UILabel *label = [[UILabel alloc] init]; - label.numberOfLines = 0; - NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:24]; - paragraphStyle.paragraphSpacing = 16; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"QMUIModalPresentationViewController支持 3 种使用方式,当前使用第 3 种,注意可以透过遮罩外的空白地方点击到背后的 cell" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColorBlack, NSParagraphStyleAttributeName: paragraphStyle}]; - NSDictionary *codeAttributes = CodeAttributes(16); - [attributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { - [attributedString addAttributes:codeAttributes range:codeRange]; - }]; - label.attributedText = attributedString; - [contentView addSubview:label]; - - UIEdgeInsets contentViewPadding = UIEdgeInsetsMake(20, 20, 20, 20); - label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding), QMUIViewSelfSizingHeight); + [self.modalViewControllerForAddSubview showInView:self.view animated:NO completion:nil]; } @end @@ -356,7 +364,7 @@ @implementation QDModalContentViewController - (void)viewDidLoad { [super viewDidLoad]; - self.view.backgroundColor = UIColorWhite; + self.view.backgroundColor = UIColor.qd_backgroundColorLighten; self.view.layer.cornerRadius = 6; self.scrollView = [[UIScrollView alloc] init]; @@ -368,7 +376,7 @@ - (void)viewDidLoad { self.imageView.contentMode = UIViewContentModeScaleAspectFill; self.imageView.clipsToBounds = YES; self.imageView.layer.borderWidth = PixelOne; - self.imageView.layer.borderColor = UIColorSeparator.CGColor; + self.imageView.layer.borderColor = UIColor.qd_separatorColor.CGColor; self.imageView.image = UIImageMake(@"image0"); [self.scrollView addSubview:self.imageView]; @@ -376,7 +384,7 @@ - (void)viewDidLoad { self.textLabel.numberOfLines = 0; NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:24]; paragraphStyle.paragraphSpacing = 16; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"如果你的浮层是以UIViewController的形式存在的,那么就可以通过modalViewController.contentViewController属性来显示出来。\n利用UIViewController的特点,你可以方便地管理复杂的UI状态,并且响应设备在不同状态下的布局。\n例如这个例子里,图片和文字的排版会随着设备的方向变化而变化,你可以试着旋转屏幕看看效果。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColorBlack, NSParagraphStyleAttributeName: paragraphStyle}]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"如果你的浮层是以UIViewController的形式存在的,那么就可以通过modalViewController.contentViewController属性来显示出来。\n利用UIViewController的特点,你可以方便地管理复杂的UI状态,并且响应设备在不同状态下的布局。\n例如这个例子里,图片和文字的排版会随着设备的方向变化而变化,你可以试着旋转屏幕看看效果。" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColor.qd_mainTextColor, NSParagraphStyleAttributeName: paragraphStyle}]; NSDictionary *codeAttributes = CodeAttributes(16); [attributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { if (![codeString isEqualToString:@"UI"]) { @@ -415,9 +423,9 @@ - (void)viewDidLayoutSubviews { #pragma mark - -- (CGSize)preferredContentSizeInModalPresentationViewController:(QMUIModalPresentationViewController *)controller limitSize:(CGSize)limitSize { +- (CGSize)preferredContentSizeInModalPresentationViewController:(QMUIModalPresentationViewController *)controller keyboardHeight:(CGFloat)keyboardHeight limitSize:(CGSize)limitSize { // 高度无穷大表示不显示高度,则默认情况下会保证你的浮层高度不超过QMUIModalPresentationViewController的高度减去contentViewMargins - return CGSizeMake(CGRectGetWidth(controller.view.bounds) - UIEdgeInsetsGetHorizontalValue(controller.view.qmui_safeAreaInsets) - UIEdgeInsetsGetHorizontalValue(controller.contentViewMargins), CGRectGetHeight(controller.view.bounds) - UIEdgeInsetsGetVerticalValue(controller.view.qmui_safeAreaInsets) - UIEdgeInsetsGetVerticalValue(controller.contentViewMargins)); + return CGSizeMake(CGRectGetWidth(controller.view.bounds) - UIEdgeInsetsGetHorizontalValue(controller.view.safeAreaInsets) - UIEdgeInsetsGetHorizontalValue(controller.contentViewMargins), CGRectGetHeight(controller.view.bounds) - UIEdgeInsetsGetVerticalValue(controller.view.safeAreaInsets) - UIEdgeInsetsGetVerticalValue(controller.contentViewMargins)); } @end diff --git a/qmuidemo/Modules/Demos/Components/QDMultipleDelegatesViewController.m b/qmuidemo/Modules/Demos/Components/QDMultipleDelegatesViewController.m index 7edb22fa..eaba9882 100644 --- a/qmuidemo/Modules/Demos/Components/QDMultipleDelegatesViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDMultipleDelegatesViewController.m @@ -59,9 +59,9 @@ - (void)initSubviews { self.textField.placeholder = @"一个输入框,多个 delegate 响应"; [self.view addSubview:self.textField]; - self.descriptionLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(12) textColor:UIColorGray6]; + self.descriptionLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(12) textColor:UIColor.qd_descriptionTextColor]; self.descriptionLabel.numberOfLines = 0; - self.descriptionLabel.attributedText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@ 对所有 NSObject 有效,这里只拿 UITextField 展示。", NSStringFromClass([QMUIMultipleDelegates class])] attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColorGray6, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:16]}];; + self.descriptionLabel.attributedText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@ 对所有 NSObject 有效,这里只拿 UITextField 展示。", NSStringFromClass([QMUIMultipleDelegates class])] attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:16]}];; [self.view addSubview:self.descriptionLabel]; } @@ -71,7 +71,7 @@ - (BOOL)shouldHideKeyboardWhenTouchInView:(UIView *)view { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - UIEdgeInsets paddings = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.qmui_safeAreaInsets.left, 24 + self.view.qmui_safeAreaInsets.bottom, 24 + self.view.qmui_safeAreaInsets.right); + UIEdgeInsets paddings = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.safeAreaInsets.left, 24 + self.view.safeAreaInsets.bottom, 24 + self.view.safeAreaInsets.right); self.descriptionLabel.frame = CGRectMake(paddings.left, paddings.top, CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(paddings), QMUIViewSelfSizingHeight); diff --git a/qmuidemo/Modules/Demos/Components/QDMultipleImagePickerPreviewViewController.m b/qmuidemo/Modules/Demos/Components/QDMultipleImagePickerPreviewViewController.m index 09225026..2a776e58 100644 --- a/qmuidemo/Modules/Demos/Components/QDMultipleImagePickerPreviewViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDMultipleImagePickerPreviewViewController.m @@ -81,7 +81,7 @@ - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; CGFloat bottomToolBarPaddingHorizontal = 12.0f; CGFloat bottomToolBarContentHeight = 44; - CGFloat bottomToolBarHeight = bottomToolBarContentHeight + self.view.qmui_safeAreaInsets.bottom; + CGFloat bottomToolBarHeight = bottomToolBarContentHeight + self.view.safeAreaInsets.bottom; self.bottomToolBarView.frame = CGRectMake(0, CGRectGetHeight(self.view.bounds) - bottomToolBarHeight, CGRectGetWidth(self.view.bounds), bottomToolBarHeight); self.sendButton.frame = CGRectSetXY(self.sendButton.frame, CGRectGetWidth(self.bottomToolBarView.frame) - bottomToolBarPaddingHorizontal - CGRectGetWidth(self.sendButton.frame), CGFloatGetCenter(bottomToolBarContentHeight, CGRectGetHeight(self.sendButton.frame))); _imageCountLabel.frame = CGRectMake(CGRectGetMinX(self.sendButton.frame) - 5 - ImageCountLabelSize.width, CGRectGetMinY(self.sendButton.frame) + CGFloatGetCenter(CGRectGetHeight(self.sendButton.frame), ImageCountLabelSize.height), ImageCountLabelSize.width, ImageCountLabelSize.height); @@ -155,7 +155,7 @@ - (void)updateOriginImageCheckboxButtonWithIndex:(NSInteger)index { - (void)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView renderZoomImageView:(QMUIZoomImageView *)zoomImageView atIndex:(NSUInteger)index { [super imagePreviewView:imagePreviewView renderZoomImageView:zoomImageView atIndex:index]; - zoomImageView.videoToolbarMargins = UIEdgeInsetsSetBottom([QMUIZoomImageView appearance].videoToolbarMargins, [QMUIZoomImageView appearance].videoToolbarMargins.bottom + CGRectGetHeight(self.bottomToolBarView.frame) - imagePreviewView.qmui_safeAreaInsets.bottom);// videToolbarMargins 是利用 UIAppearance 赋值的,也即意味着要在 addSubview 之后才会被赋值,而在 renderZoomImageView 里,zoomImageView 可能尚未被添加到 view 层级里,所以无法通过 zoomImageView.videoToolbarMargins 获取到原来的值,因此只能通过 [QMUIZoomImageView appearance] 的方式获取 + zoomImageView.videoToolbarMargins = UIEdgeInsetsSetBottom([QMUIZoomImageView appearance].videoToolbarMargins, [QMUIZoomImageView appearance].videoToolbarMargins.bottom + CGRectGetHeight(self.bottomToolBarView.frame) - imagePreviewView.safeAreaInsets.bottom);// videToolbarMargins 是利用 UIAppearance 赋值的,也即意味着要在 addSubview 之后才会被赋值,而在 renderZoomImageView 里,zoomImageView 可能尚未被添加到 view 层级里,所以无法通过 zoomImageView.videoToolbarMargins 获取到原来的值,因此只能通过 [QMUIZoomImageView appearance] 的方式获取 } - (void)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView willScrollHalfToIndex:(NSUInteger)index { diff --git a/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingAnimatorViewController.m b/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingAnimatorViewController.m index 1ffd53d8..771ab26e 100644 --- a/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingAnimatorViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingAnimatorViewController.m @@ -41,6 +41,7 @@ - (void)viewDidLoad { } - (UIStatusBarStyle)preferredStatusBarStyle { + // 需要手动调用 navigationAnimator.statusbarStyleBlock 来告诉系统状态栏的变化 if (self.navigationAnimator) { return self.navigationAnimator.statusbarStyleBlock(self.navigationAnimator, self.navigationAnimator.progress); } @@ -51,20 +52,20 @@ - (UIStatusBarStyle)preferredStatusBarStyle { #pragma mark - -- (UIImage *)navigationBarBackgroundImage { +- (UIImage *)qmui_navigationBarBackgroundImage { return self.navigationAnimator.backgroundImageBlock(self.navigationAnimator, self.navigationAnimator.progress); } -- (UIImage *)navigationBarShadowImage { +- (UIImage *)qmui_navigationBarShadowImage { return self.navigationAnimator.shadowImageBlock(self.navigationAnimator, self.navigationAnimator.progress); } -- (UIColor *)navigationBarTintColor { +- (UIColor *)qmui_navigationBarTintColor { return self.navigationAnimator.tintColorBlock(self.navigationAnimator, self.navigationAnimator.progress); } -- (UIColor *)titleViewTintColor { - return [self navigationBarTintColor]; +- (UIColor *)qmui_titleViewTintColor { + return [self qmui_navigationBarTintColor]; } #pragma mark - diff --git a/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingSnapAnimatorViewController.m b/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingSnapAnimatorViewController.m index b793ba6a..938a4062 100644 --- a/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingSnapAnimatorViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingSnapAnimatorViewController.m @@ -28,16 +28,12 @@ - (void)viewDidLoad { }; // 为了避免更改 navigationBar 显隐影响 scrollView 的滚动,这里屏蔽掉自动适应 contentInset - if (@available(iOS 11, *)) { - self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } else { - self.automaticallyAdjustsScrollViewInsets = NO; - } + self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - self.tableView.contentInset = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator, 0, self.view.qmui_safeAreaInsets.bottom, 0); + self.tableView.contentInset = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator, 0, self.view.safeAreaInsets.bottom, 0); self.tableView.scrollIndicatorInsets = self.tableView.contentInset; [self.tableView qmui_scrollToTopUponContentInsetTopChange]; } diff --git a/qmuidemo/Modules/Demos/Components/QDNavigationTitleViewController.m b/qmuidemo/Modules/Demos/Components/QDNavigationTitleViewController.m index 7a616c36..dda6fb28 100644 --- a/qmuidemo/Modules/Demos/Components/QDNavigationTitleViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDNavigationTitleViewController.m @@ -40,9 +40,9 @@ - (void)initPopupContainerViewIfNeeded { self.popupMenuView.automaticallyHidesWhenUserTap = YES;// 点击空白地方自动消失 self.popupMenuView.preferLayoutDirection = QMUIPopupContainerViewLayoutDirectionBelow; self.popupMenuView.maximumWidth = 220; - self.popupMenuView.items = @[[QMUIPopupMenuButtonItem itemWithImage:UIImageMake(@"icon_emotion") title:@"分类 1" handler:nil], - [QMUIPopupMenuButtonItem itemWithImage:UIImageMake(@"icon_emotion") title:@"分类 2" handler:nil], - [QMUIPopupMenuButtonItem itemWithImage:UIImageMake(@"icon_emotion") title:@"分类 3" handler:nil]]; + self.popupMenuView.items = @[[QMUIPopupMenuItem itemWithImage:UIImageMake(@"icon_emotion") title:@"分类 1" handler:nil], + [QMUIPopupMenuItem itemWithImage:UIImageMake(@"icon_emotion") title:@"分类 2" handler:nil], + [QMUIPopupMenuItem itemWithImage:UIImageMake(@"icon_emotion") title:@"分类 3" handler:nil]]; self.popupMenuView.sourceView = self.titleView; __weak __typeof(self)weakSelf = self; self.popupMenuView.didHideBlock = ^(BOOL hidesByUserTap) { diff --git a/qmuidemo/Modules/Demos/Components/QDPieProgressViewController.m b/qmuidemo/Modules/Demos/Components/QDPieProgressViewController.m index c2855041..32e3c8fc 100644 --- a/qmuidemo/Modules/Demos/Components/QDPieProgressViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDPieProgressViewController.m @@ -14,7 +14,7 @@ @interface QDPieProgressViewController () @property(nonatomic, strong) UIView *section1; @property(nonatomic, strong) QMUIPieProgressView *progressView1; -@property(nonatomic, strong) QMUISlider *slider; +@property(nonatomic, strong) UISlider *slider; @property(nonatomic, strong) UILabel *titleLabel1; @property(nonatomic, strong) UIView *section2; @@ -31,11 +31,6 @@ @interface QDPieProgressViewController () @implementation QDPieProgressViewController -- (void)viewDidLoad { - [super viewDidLoad]; - self.view.backgroundColor = UIColorWhite; -} - - (void)initSubviews { [super initSubviews]; @@ -49,7 +44,7 @@ - (void)initSubviews { [self.scrollView addSubview:self.section1]; self.progressView1 = [[QMUIPieProgressView alloc] initWithFrame:CGRectMake(0, 0, 75, 75)]; - self.progressView1.tintColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + self.progressView1.tintColor = UIColor.qd_tintColor; self.progressView1.progressAnimationDuration = .2; [self.progressView1 addTarget:self action:@selector(handleProgressViewValueChanged:) forControlEvents:UIControlEventValueChanged]; [self.section1 addSubview:self.progressView1]; @@ -59,13 +54,11 @@ - (void)initSubviews { self.titleLabel1.textAlignment = NSTextAlignmentCenter; [self.section1 addSubview:self.titleLabel1]; - self.slider = [[QMUISlider alloc] init]; + self.slider = [[UISlider alloc] init]; self.slider.tintColor = self.progressView1.tintColor; - self.slider.thumbSize = CGSizeMake(16, 16); - self.slider.thumbColor = self.slider.tintColor; - self.slider.thumbShadowColor = [self.slider.tintColor colorWithAlphaComponent:.3]; - self.slider.thumbShadowOffset = CGSizeMake(0, 2); - self.slider.thumbShadowRadius = 3; + self.slider.qmui_thumbSize = CGSizeMake(16, 16); + self.slider.qmui_thumbColor = self.slider.tintColor; + self.slider.qmui_thumbShadow = [NSShadow qmui_shadowWithColor:[self.slider.tintColor colorWithAlphaComponent:.3] shadowOffset:CGSizeMake(0, 2) shadowRadius:3]; [self.slider sizeToFit]; [self.slider addTarget:self action:@selector(handleSliderTouchUpInside:) forControlEvents:UIControlEventValueChanged]; [self.section1 addSubview:self.slider]; @@ -77,13 +70,13 @@ - (void)initSubviews { [self.scrollView addSubview:self.section2]; self.progressView2 = [[QMUIPieProgressView alloc] initWithFrame:CGRectMake(0, 0, 60, 60)]; - self.progressView2.tintColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + self.progressView2.tintColor = UIColor.qd_tintColor; self.progressView2.borderWidth = 2; self.progressView2.borderInset = 3; [self.progressView2 setProgress:.3]; [self.section2 addSubview:self.progressView2]; - self.titleLabel2 = [[UILabel alloc] qmui_initWithFont:UIFontMake(11) textColor:UIColorGray6]; + self.titleLabel2 = [[UILabel alloc] qmui_initWithFont:UIFontMake(11) textColor:UIColor.qd_descriptionTextColor]; self.titleLabel2.numberOfLines = 0; self.titleLabel2.text = @"通过 borderWidth、borderInset 修改样式"; [self.titleLabel2 sizeToFit]; @@ -111,7 +104,7 @@ - (void)initSubviews { [self.progressView33 setProgress:.28]; [self.section3 addSubview:self.progressView33]; - self.titleLabel3 = [[UILabel alloc] qmui_initWithFont:UIFontMake(11) textColor:UIColorGray6]; + self.titleLabel3 = [[UILabel alloc] qmui_initWithFont:UIFontMake(11) textColor:UIColor.qd_descriptionTextColor]; self.titleLabel3.numberOfLines = 0; self.titleLabel3.text = @"通过 backgroundColor 或 tintColor 修改颜色"; [self.titleLabel3 sizeToFit]; @@ -129,7 +122,7 @@ - (void)viewDidLayoutSubviews { self.scrollView.frame = self.view.bounds; - CGFloat horizontalInset = 25 + self.scrollView.qmui_safeAreaInsets.left; + CGFloat horizontalInset = 25 + self.scrollView.safeAreaInsets.left; CGFloat sectionHeight = 145; CGFloat progressView1MarginRight = 30; diff --git a/qmuidemo/Modules/Demos/Components/QDPopupContainerViewController.m b/qmuidemo/Modules/Demos/Components/QDPopupContainerViewController.m index 4b7e8c90..3c665a97 100644 --- a/qmuidemo/Modules/Demos/Components/QDPopupContainerViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDPopupContainerViewController.m @@ -23,6 +23,16 @@ - (instancetype)initWithFrame:(CGRect)frame { self.emotionInputManager.emotionView.emotions = [QDUIHelper qmuiEmotions]; self.emotionInputManager.emotionView.sendButton.hidden = YES; [self.contentView addSubview:self.emotionInputManager.emotionView]; + self.emotionInputManager.emotionView.backgroundColor = nil; + self.maskViewBackgroundColor = nil; + self.backgroundColor = nil; + self.borderWidth = 0; + self.arrowImage = UIImageMake(@"popover_container_arrow"); + self.backgroundView = ({ + UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]]; + effectView.qmui_foregroundColor = [UIColor.blackColor colorWithAlphaComponent:.2]; + effectView; + }); } return self; } @@ -44,12 +54,15 @@ @interface QDPopupContainerViewController () @property(nonatomic, strong) QMUIButton *button1; @property(nonatomic, strong) QMUIPopupContainerView *popupByAddSubview; @property(nonatomic, strong) QMUIButton *button2; -@property(nonatomic, strong) QMUIPopupMenuView *popupByWindow; @property(nonatomic, strong) QMUIButton *button3; +@property(nonatomic, strong) QMUIPopupContainerView *popupHorizontal; +@property(nonatomic, strong) QMUIButton *button4; +@property(nonatomic, strong) QMUIPopupContainerView *popupByWindow; +@property(nonatomic, strong) QMUIButton *button5; @property(nonatomic, strong) QDPopupContainerView *popupWithCustomView; @property(nonatomic, strong) CALayer *separatorLayer1; @property(nonatomic, strong) CALayer *separatorLayer2; -@property(nonatomic, strong) QMUIPopupMenuView *popupAtBarButtonItem; +@property(nonatomic, strong) QMUIPopupContainerView *popupAtBarButtonItem; @end @implementation QDPopupContainerViewController @@ -74,10 +87,43 @@ - (void)initSubviews { [self.button1 setTitle:@"显示默认浮层" forState:UIControlStateNormal]; [self.view addSubview:self.button1]; + self.button2 = [QDUIHelper generateLightBorderedButton]; + [self.button2 addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.button2 setImage:TableViewCellDisclosureIndicatorImage forState:UIControlStateNormal]; + [self.button2 setTitle:@"右边" forState:UIControlStateNormal]; + self.button2.imagePosition = QMUIButtonImagePositionRight; + self.button2.spacingBetweenImageAndTitle = 4; + [self.view addSubview:self.button2]; + + self.button3 = [QDUIHelper generateLightBorderedButton]; + [self.button3 addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.button3 setImage:[TableViewCellDisclosureIndicatorImage qmui_imageWithOrientation:UIImageOrientationRightMirrored] forState:UIControlStateNormal]; + [self.button3 setTitle:@"左边" forState:UIControlStateNormal]; + self.button3.imagePosition = QMUIButtonImagePositionLeft; + self.button3.spacingBetweenImageAndTitle = 4; + [self.view addSubview:self.button3]; + + self.button4 = [QDUIHelper generateLightBorderedButton]; + [self.button4 addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.button4 setTitle:@"显示菜单浮层" forState:UIControlStateNormal]; + [self.view addSubview:self.button4]; + + self.button5 = [QDUIHelper generateLightBorderedButton]; + [self.button5 addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.button5 setTitle:@"显示自定义浮层" forState:UIControlStateNormal]; + [self.view addSubview:self.button5]; + + + + + // 使用方法 1,以 addSubview: 的形式显示到界面上 self.popupByAddSubview = [[QMUIPopupContainerView alloc] init]; - self.popupByAddSubview.imageView.image = [[UIImageMake(@"icon_emotion") qmui_imageResizedInLimitedSize:CGSizeMake(24, 24) resizingMode:QMUIImageResizingModeScaleToFill] qmui_imageWithTintColor:[QDThemeManager sharedInstance].currentTheme.themeTintColor]; + self.popupByAddSubview.imageView.image = [UIImage qmui_imageWithThemeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return [[UIImageMake(@"icon_emotion") qmui_imageResizedInLimitedSize:CGSizeMake(24, 24) resizingMode:QMUIImageResizingModeScaleToFill] qmui_imageWithTintColor:theme.themeTintColor]; + }]; self.popupByAddSubview.textLabel.text = @"默认自带 imageView、textLabel,可展示简单的内容"; + self.popupByAddSubview.textLabel.textColor = UIColor.qd_mainTextColor; self.popupByAddSubview.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 8); self.popupByAddSubview.didHideBlock = ^(BOOL hidesByUserTap) { [weakSelf.button1 setTitle:@"显示默认浮层" forState:UIControlStateNormal]; @@ -87,57 +133,43 @@ - (void)initSubviews { self.popupByAddSubview.hidden = YES; [self.view addSubview:self.popupByAddSubview]; + self.popupHorizontal = [[QMUIPopupContainerView alloc] init]; + self.popupHorizontal.imageView.image = [UIImage qmui_imageWithThemeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return [[UIImageMake(@"icon_emotion") qmui_imageResizedInLimitedSize:CGSizeMake(24, 24) resizingMode:QMUIImageResizingModeScaleToFill] qmui_imageWithTintColor:theme.themeTintColor]; + }]; + self.popupHorizontal.textLabel.text = @"可通过 contentMode 调整内容的布局。\n这样在文字比较长的时候就能看到区别。\n不够长再换几次行。\n不够长再换几次行。\n不够长再换几次行。"; + self.popupHorizontal.textLabel.textColor = UIColor.qd_mainTextColor; + self.popupHorizontal.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 8); + self.popupHorizontal.hidden = YES; + [self.view addSubview:self.popupHorizontal]; - self.button2 = [QDUIHelper generateLightBorderedButton]; - [self.button2 addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self.button2 setTitle:@"显示菜单浮层" forState:UIControlStateNormal]; - [self.view addSubview:self.button2]; - // 使用方法 2,以 UIWindow 的形式显示到界面上,这种无需默认隐藏,也无需 add 到某个 UIView 上 - self.popupByWindow = [[QMUIPopupMenuView alloc] init]; + self.popupByWindow = [[QMUIPopupContainerView alloc] init]; self.popupByWindow.automaticallyHidesWhenUserTap = YES;// 点击空白地方消失浮层 - self.popupByWindow.maskViewBackgroundColor = UIColorMaskWhite;// 使用方法 2 并且打开了 automaticallyHidesWhenUserTap 的情况下,可以修改背景遮罩的颜色 - self.popupByWindow.shouldShowItemSeparator = YES; - self.popupByWindow.itemConfigurationHandler = ^(QMUIPopupMenuView *aMenuView, QMUIPopupMenuButtonItem *aItem, NSInteger section, NSInteger index) { - // 利用 itemConfigurationHandler 批量设置所有 item 的样式 - aItem.button.highlightedBackgroundColor = [[QDThemeManager sharedInstance].currentTheme.themeTintColor colorWithAlphaComponent:.2]; - }; - self.popupByWindow.items = @[[QMUIPopupMenuButtonItem itemWithImage:[UIImageMake(@"icon_tabbar_uikit") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:@"QMUIKit" handler:^(QMUIPopupMenuButtonItem *aItem) { - [aItem.menuView hideWithAnimated:YES]; - }], - [QMUIPopupMenuButtonItem itemWithImage:[UIImageMake(@"icon_tabbar_component") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:@"Components" handler:^(QMUIPopupMenuButtonItem *aItem) { - [aItem.menuView hideWithAnimated:YES]; - }], - [QMUIPopupMenuButtonItem itemWithImage:[UIImageMake(@"icon_tabbar_lab") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:@"Lab" handler:^(QMUIPopupMenuButtonItem *aItem) { - [aItem.menuView hideWithAnimated:YES]; - }]]; + self.popupByWindow.tintColor = UIColor.qd_tintColor; + self.popupByWindow.maskViewBackgroundColor = [UIColor.qd_tintColor colorWithAlphaComponent:.15];// 使用方法 2 并且打开了 automaticallyHidesWhenUserTap 的情况下,可以修改背景遮罩的颜色 + self.popupByWindow.textLabel.text = @"以 window 形式显示,文字写得长一点才能显得好看一点"; + self.popupByWindow.textLabel.textColor = UIColor.qd_mainTextColor; + self.popupByWindow.textLabel.font = UIFontMake(16); self.popupByWindow.didHideBlock = ^(BOOL hidesByUserTap) { - [weakSelf.button2 setTitle:@"显示菜单浮层" forState:UIControlStateNormal]; + [weakSelf.button4 setTitle:@"显示菜单浮层" forState:UIControlStateNormal]; }; - self.popupByWindow.sourceView = self.button2;// 相对于 button2 布局 - - - - self.button3 = [QDUIHelper generateLightBorderedButton]; - [self.button3 addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self.button3 setTitle:@"显示自定义浮层" forState:UIControlStateNormal]; - [self.view addSubview:self.button3]; + self.popupByWindow.sourceView = self.button4;// 相对于 button4 布局 + self.popupWithCustomView = [[QDPopupContainerView alloc] init]; self.popupWithCustomView.preferLayoutDirection = QMUIPopupContainerViewLayoutDirectionBelow;// 默认在目标的下方,如果目标下方空间不够,会尝试放到目标上方。若上方空间也不够,则缩小自身的高度。 self.popupWithCustomView.didHideBlock = ^(BOOL hidesByUserTap) { - [weakSelf.button3 setTitle:@"显示自定义浮层" forState:UIControlStateNormal]; + [weakSelf.button5 setTitle:@"显示自定义浮层" forState:UIControlStateNormal]; }; // 在 UIBarButtonItem 上显示 - self.popupAtBarButtonItem = [[QMUIPopupMenuView alloc] init]; + self.popupAtBarButtonItem = [[QMUIPopupContainerView alloc] init]; self.popupAtBarButtonItem.automaticallyHidesWhenUserTap = YES;// 点击空白地方消失浮层 - self.popupAtBarButtonItem.maximumWidth = 180; - self.popupAtBarButtonItem.shouldShowItemSeparator = YES; - self.popupAtBarButtonItem.items = @[[QMUIPopupMenuButtonItem itemWithImage:[UIImageMake(@"icon_tabbar_uikit") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:@"QMUIKit" handler:NULL], - [QMUIPopupMenuButtonItem itemWithImage:[UIImageMake(@"icon_tabbar_component") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:@"Components" handler:NULL], - [QMUIPopupMenuButtonItem itemWithImage:[UIImageMake(@"icon_tabbar_lab") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:@"Lab" handler:NULL]]; + self.popupAtBarButtonItem.textLabel.textColor = UIColor.qd_mainTextColor; + self.popupAtBarButtonItem.textLabel.font = UIFontMake(16); + self.popupAtBarButtonItem.textLabel.text = @"指向某个 UIBarButtonItem"; } - (void)setupNavigationItems { @@ -160,17 +192,20 @@ - (void)viewDidLayoutSubviews { CGFloat viewportHeight = CGRectGetHeight(self.view.bounds) - minY; CGFloat sectionHeight = viewportHeight / 3.0; - self.popupByAddSubview.safetyMarginsOfSuperview = UIEdgeInsetsSetTop(self.popupByAddSubview.safetyMarginsOfSuperview, self.qmui_navigationBarMaxYInViewCoordinator + 10); - self.button1.frame = CGRectSetXY(self.button1.frame, CGFloatGetCenter(CGRectGetWidth(self.view.bounds), CGRectGetWidth(self.button1.frame)), minY + (sectionHeight - CGRectGetHeight(self.button1.frame)) / 2.0); + CGFloat buttonMargin = 8; + CGFloat sectionContentHeight = CGRectGetHeight(self.button1.frame) * 2 + buttonMargin; + self.button1.frame = CGRectSetXY(self.button1.frame, CGFloatGetCenter(CGRectGetWidth(self.view.bounds), CGRectGetWidth(self.button1.frame)), minY + CGFloatGetCenter(sectionHeight, sectionContentHeight)); + self.button2.frame = CGRectMake(CGRectGetMinX(self.button1.frame), CGRectGetMaxY(self.button1.frame) + buttonMargin, (CGRectGetWidth(self.button1.frame) - buttonMargin) / 2, CGRectGetHeight(self.button2.frame)); + self.button3.frame = CGRectSetX(self.button2.frame, CGRectGetMaxX(self.button2.frame) + buttonMargin); self.separatorLayer1.frame = CGRectFlatMake(0, minY + sectionHeight, CGRectGetWidth(self.view.bounds), PixelOne); - self.button2.frame = CGRectSetY(self.button1.frame, CGRectGetMaxY(self.button1.frame) + sectionHeight - CGRectGetHeight(self.button2.frame)); + self.button4.frame = CGRectSetY(self.button1.frame, CGRectGetMaxY(self.button1.frame) + sectionHeight - CGRectGetHeight(self.button4.frame)); self.separatorLayer2.frame = CGRectSetY(self.separatorLayer1.frame, minY + sectionHeight * 2.0); - self.button3.frame = CGRectSetY(self.button1.frame, CGRectGetMaxY(self.button2.frame) + sectionHeight - CGRectGetHeight(self.button3.frame)); - self.popupWithCustomView.sourceRect = [self.button3 convertRect:self.button3.bounds toView:nil];// 将 button3 的坐标转换到相对于 UIWindow 的坐标系里,然后再传给浮层布局 + self.button5.frame = CGRectSetY(self.button1.frame, CGRectGetMaxY(self.button4.frame) + sectionHeight - CGRectGetHeight(self.button5.frame)); + self.popupWithCustomView.sourceRect = [self.button5 convertRect:self.button5.bounds toView:nil];// 将 button3 的坐标转换到相对于 UIWindow 的坐标系里,然后再传给浮层布局 } - (void)handleButtonEvent:(QMUIButton *)button { @@ -186,17 +221,41 @@ - (void)handleButtonEvent:(QMUIButton *)button { } if (button == self.button2) { - [self.popupByWindow showWithAnimated:YES]; - [self.button2 setTitle:@"隐藏菜单浮层" forState:UIControlStateNormal]; + if (self.popupHorizontal.isShowing) { + [self.popupHorizontal hideWithAnimated:YES]; + } else { + self.popupHorizontal.sourceView = button; + self.popupHorizontal.preferLayoutDirection = QMUIPopupContainerViewLayoutDirectionRight; + self.popupHorizontal.contentMode = UIViewContentModeTop; + [self.popupHorizontal showWithAnimated:YES]; + } return; } if (button == self.button3) { + if (self.popupHorizontal.isShowing) { + [self.popupHorizontal hideWithAnimated:YES]; + } else { + self.popupHorizontal.sourceView = button; + self.popupHorizontal.preferLayoutDirection = QMUIPopupContainerViewLayoutDirectionLeft; + self.popupHorizontal.contentMode = UIViewContentModeBottom; + [self.popupHorizontal showWithAnimated:YES]; + } + return; + } + + if (button == self.button4) { + [self.popupByWindow showWithAnimated:YES]; + [self.button4 setTitle:@"隐藏菜单浮层" forState:UIControlStateNormal]; + return; + } + + if (button == self.button5) { if (self.popupWithCustomView.isShowing) { [self.popupWithCustomView hideWithAnimated:YES]; } else { [self.popupWithCustomView showWithAnimated:YES]; - [self.button3 setTitle:@"隐藏自定义浮层" forState:UIControlStateNormal]; + [self.button5 setTitle:@"隐藏自定义浮层" forState:UIControlStateNormal]; } return; } diff --git a/qmuidemo/Modules/Demos/Components/QDPopupMenuViewController.h b/qmuidemo/Modules/Demos/Components/QDPopupMenuViewController.h new file mode 100644 index 00000000..aa9e32a3 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDPopupMenuViewController.h @@ -0,0 +1,17 @@ +// +// QDPopupMenuViewController.h +// qmuidemo +// +// Created by molice on 2024/8/1. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDPopupMenuViewController : QDCommonViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Components/QDPopupMenuViewController.m b/qmuidemo/Modules/Demos/Components/QDPopupMenuViewController.m new file mode 100644 index 00000000..1807550e --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDPopupMenuViewController.m @@ -0,0 +1,320 @@ +// +// QDPopupMenuViewController.m +// qmuidemo +// +// Created by molice on 2024/8/1. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import "QDPopupMenuViewController.h" +#import "QMUIInteractiveDebugger.h" + +@interface QDPopupMenuViewController () +@property(nonatomic, strong) UIScrollView *scrollView; +@property(nonatomic, strong) QMUIButton *actionButton; +@property(nonatomic, strong) QMUIInteractiveDebugPanelViewController *debugViewController; + +@property(nonatomic, strong) QMUIPopupMenuView *menu; +@property(nonatomic, assign) BOOL shouldShowMultipleSections; +@property(nonatomic, assign) BOOL shouldShowSectionTitles; +@property(nonatomic, assign) BOOL useBigData; +@end + +@implementation QDPopupMenuViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.scrollView = UIScrollView.new; + [self.view addSubview:self.scrollView]; + + self.actionButton = QDUIHelper.generateLightBorderedButton; + [self.actionButton setTitle:@"显示浮层" forState:UIControlStateNormal]; + [self.actionButton addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.scrollView addSubview:self.actionButton]; + + self.menu = [[QMUIPopupMenuView alloc] init]; + self.menu.maskViewBackgroundColor = nil; + self.menu.automaticallyHidesWhenUserTap = NO; + self.menu.maximumHeight = 400; + [self updateMenu]; + self.menu.sourceView = self.actionButton; + self.menu.hidden = YES; + [self.scrollView addSubview:self.menu]; + + __weak __typeof(self)weakSelf = self; + __weak __typeof(self.menu)weakMenu = self.menu; + + self.menu.willShowBlock = ^(BOOL animated) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((animated ? .3 : 0) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [weakSelf.actionButton setTitle:weakMenu.isShowing ? @"隐藏 Menu" : @"显示 Menu" forState:UIControlStateNormal]; + [weakSelf updateLayoutAnimated:YES]; + }); + }; + self.menu.didHideBlock = ^(BOOL hidesByUserTap) { + [weakSelf.actionButton setTitle:weakMenu.isShowing ? @"隐藏 Menu" : @"显示 Menu" forState:UIControlStateNormal]; + [weakSelf updateLayoutAnimated:YES]; + }; + + self.debugViewController = [QDUIHelper generateDebugViewControllerWithTitle:@"配置参数" items:@[ + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"显示箭头" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakMenu.arrowSize.height > 0; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakMenu.arrowSize = actionView.on ? QMUIPopupContainerView.appearance.arrowSize : CGSizeZero; + [weakSelf updateLayoutAnimated:YES]; + }], + [QMUIInteractiveDebugPanelItem enumItemWithTitle:@"item 高度" items:@[@"固定(44)", @"内容自适应"] valueGetter:^(QMUIButton * _Nonnull actionView, NSArray * _Nonnull items) { + [actionView setTitle:items[weakMenu.itemHeight == QMUIViewSelfSizingHeight ? 1 : 0] forState:UIControlStateNormal]; + } valueSetter:^(QMUIButton * _Nonnull actionView, NSArray * _Nonnull items) { + NSInteger index = [items indexOfObject:actionView.currentTitle]; + weakMenu.itemHeight = index == 1 ? QMUIViewSelfSizingHeight : 44; + [weakSelf updateLayoutAnimated:YES]; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"显示 item 分隔线" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakMenu.shouldShowItemSeparator; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakMenu.shouldShowItemSeparator = actionView.on; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"分段" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakSelf.shouldShowMultipleSections; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakSelf.shouldShowMultipleSections = actionView.on; + [weakSelf updateLayoutAnimated:YES]; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"显示分段分隔线" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakMenu.shouldShowSectionSeparator; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakMenu.shouldShowSectionSeparator = actionView.on; + [weakSelf updateLayoutAnimated:YES]; + }], + [QMUIInteractiveDebugPanelItem numbericItemWithTitle:@"分段分隔大小" valueGetter:^(QMUITextField * _Nonnull actionView) { + actionView.text = [NSString stringWithFormat:@"%.0f", weakMenu.sectionSpacing]; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + weakMenu.sectionSpacing = actionView.text.doubleValue; + [weakSelf updateLayoutAnimated:YES]; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"支持选中(默认单选)" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakMenu.allowsSelection; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakMenu.allowsSelection = actionView.on; + if (weakMenu.allowsSelection) { + weakMenu.selectedItemIndex = 0; + } + if (!weakMenu.allowsSelection) { + UISwitch *switcher = (UISwitch *)[weakSelf.debugViewController itemMatched:^BOOL(__kindof QMUIInteractiveDebugPanelItem * _Nonnull item) { + return [item.title isEqualToString:@"多选"]; + }].actionView; + switcher.on = NO; + } + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"多选" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakMenu.allowsMultipleSelection; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakMenu.allowsMultipleSelection = actionView.on; + if (weakMenu.allowsMultipleSelection) { + weakMenu.selectedItemIndexPaths = @[ + [NSIndexPath indexPathForRow:0 inSection:0], + [NSIndexPath indexPathForRow:1 inSection:0], + ]; + } + UISwitch *switcher = (UISwitch *)[weakSelf.debugViewController itemMatched:^BOOL(__kindof QMUIInteractiveDebugPanelItem * _Nonnull item) { + return [item.title isEqualToString:@"支持选中(默认单选)"]; + }].actionView; + switcher.on = weakMenu.allowsSelection; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"指定 item 不支持选中" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = !!weakMenu.shouldSelectItemBlock; + } valueSetter:^(UISwitch * _Nonnull actionView) { + if (actionView.on) { + weakMenu.shouldSelectItemBlock = ^BOOL(__kindof QMUIPopupMenuItem * _Nonnull aItem, __kindof UIControl * _Nonnull aItemView, NSInteger section, NSInteger index) { + return section != 0 || index != 0;// 第一个 item 不支持选中 + }; + if ([weakMenu.selectedItemIndexPaths containsObject:[NSIndexPath indexPathForRow:0 inSection:0]]) { + NSMutableArray *indexPaths = weakMenu.selectedItemIndexPaths.mutableCopy; + [indexPaths removeObject:[NSIndexPath indexPathForRow:0 inSection:0]]; + weakMenu.selectedItemIndexPaths = indexPaths.copy; + } + } else { + weakMenu.shouldSelectItemBlock = nil; + } + [weakSelf updateMenu]; + }], + [QMUIInteractiveDebugPanelItem enumItemWithTitle:@"选中样式" items:@[@"Checkmark", @"Checkbox"] valueGetter:^(QMUIButton * _Nonnull actionView, NSArray * _Nonnull items) { + [actionView setTitle:items[weakMenu.selectedStyle] forState:UIControlStateNormal]; + } valueSetter:^(QMUIButton * _Nonnull actionView, NSArray * _Nonnull items) { + NSInteger index = [items indexOfObject:actionView.currentTitle]; + weakMenu.selectedStyle = index; + }], + [QMUIInteractiveDebugPanelItem enumItemWithTitle:@"选中布局" items:@[@"AtEnd", @"AtStart"] valueGetter:^(QMUIButton * _Nonnull actionView, NSArray * _Nonnull items) { + [actionView setTitle:items[weakMenu.selectedLayout] forState:UIControlStateNormal]; + } valueSetter:^(QMUIButton * _Nonnull actionView, NSArray * _Nonnull items) { + NSInteger index = [items indexOfObject:actionView.currentTitle]; + weakMenu.selectedLayout = index; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"宽度自适应内容" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakMenu.adjustsWidthAutomatically; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakMenu.adjustsWidthAutomatically = actionView.on; + [weakSelf updateLayoutAnimated:YES]; + }], + [QMUIInteractiveDebugPanelItem numbericItemWithTitle:@"最小宽度" valueGetter:^(QMUITextField * _Nonnull actionView) { + actionView.text = [NSString stringWithFormat:@"%.0f", weakMenu.minimumWidth]; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + weakMenu.minimumWidth = actionView.text.doubleValue; + }], + [QMUIInteractiveDebugPanelItem numbericItemWithTitle:@"最大宽度" valueGetter:^(QMUITextField * _Nonnull actionView) { + actionView.text = weakMenu.maximumWidth == CGFLOAT_MAX ? @"CGFLOAT_MAX" : [NSString stringWithFormat:@"%.0f", weakMenu.maximumWidth]; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + weakMenu.maximumWidth = [actionView.text isEqualToString:@"CGFLOAT_MAX"] ? CGFLOAT_MAX : actionView.text.doubleValue; + }], + [QMUIInteractiveDebugPanelItem enumItemWithTitle:@"对齐目标位置" items:@[@"Center", @"Leading", @"Trailing"] valueGetter:^(QMUIButton * _Nonnull actionView, NSArray * _Nonnull items) { + [actionView setTitle:items[weakMenu.preferLayoutAlignment] forState:UIControlStateNormal]; + } valueSetter:^(QMUIButton * _Nonnull actionView, NSArray * _Nonnull items) { + QMUIPopupContainerViewLayoutAlignment alignment = (QMUIPopupContainerViewLayoutAlignment)[items indexOfObject:actionView.currentTitle]; + weakMenu.preferLayoutAlignment = alignment; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"显示底部附加 view" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = !!weakMenu.bottomAccessoryView; + } valueSetter:^(UISwitch * _Nonnull actionView) { + if (actionView.on) { + QMUIButton *button = [[QMUIButton alloc] qmui_initWithImage:[UIImageMake(@"icon_nav_about") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:@"其他"]; + button.titleLabel.font = UIFontMake(16); + button.contentEdgeInsets = UIEdgeInsetsMake(8, weakMenu.padding.left, 8, weakMenu.padding.right); + button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; + button.spacingBetweenImageAndTitle = 12; + button.highlightedBackgroundColor = TableViewCellSelectedBackgroundColor; + button.adjustsTitleTintColorAutomatically = YES; + button.tintColor = UIColorBlue; + button.qmui_borderPosition = QMUIViewBorderPositionTop; + button.qmui_tapBlock = ^(__kindof UIControl *sender) { + [weakMenu hideWithAnimated:YES]; + }; + weakMenu.bottomAccessoryView = button; + } else { + weakMenu.bottomAccessoryView = nil; + } + [weakSelf updateLayoutAnimated:YES]; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"测试大数据" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakSelf.useBigData; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakSelf.useBigData = actionView.on; + [weakSelf updateLayoutAnimated:YES]; + }], + ]]; + [self.scrollView addSubview:self.debugViewController.view]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [self.menu showWithAnimated:NO]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + [self.menu hideWithAnimated:NO]; +} + +- (void)handleButtonEvent:(QMUIButton *)button { + if (self.menu.isShowing) { + [self.menu hideWithAnimated:YES]; + } else { + self.menu.sourceBarItem = nil; + self.menu.sourceView = button; + [self.menu showWithAnimated:YES]; + } +} + +- (void)setShouldShowMultipleSections:(BOOL)shouldShowMultipleSections { + _shouldShowMultipleSections = shouldShowMultipleSections; + [self updateMenu]; +} + +- (void)setShouldShowSectionTitles:(BOOL)shouldShowSectionTitles { + _shouldShowSectionTitles = shouldShowSectionTitles; + if (shouldShowSectionTitles) { + _shouldShowMultipleSections = YES; + } + [self updateMenu]; +} + +- (void)setUseBigData:(BOOL)useBigData { + _useBigData = useBigData; + [self updateMenu]; +} + +- (void)updateMenu { + void (^handler)(__kindof QMUIPopupMenuItem * _Nonnull aItem, __kindof UIControl * _Nonnull aItemView, NSInteger section, NSInteger index) = ^void(__kindof QMUIPopupMenuItem * _Nonnull aItem, __kindof UIControl * _Nonnull aItemView, NSInteger section, NSInteger index) { + if (!aItem.menuView.allowsSelection) { + [aItem.menuView hideWithAnimated:YES]; + } + }; + + NSMutableArray *items = NSMutableArray.new; + + if (self.shouldShowMultipleSections) { + // section0 + [items addObject:@[ + [QMUIPopupMenuItem itemWithImage:[UIImageMake(@"icon_tabbar_uikit") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:self.menu.shouldSelectItemBlock ? @"我不可被选中" : @"选项0" handler:handler], + ]]; + + // section1 + NSMutableArray *section1 = NSMutableArray.new; + if (self.useBigData) { + for (NSInteger i = 0; i < 200; i++) { + [section1 addObject:[QMUIPopupMenuItem itemWithImage:[UIImageMake(@"icon_tabbar_component") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:[NSString stringWithFormat:@"选项%@", @(i)] subtitle:i % 2 == 0 ? @"副标题" : nil handler:handler]]; + } + } else { + [section1 addObjectsFromArray:@[ + [QMUIPopupMenuItem itemWithImage:[UIImageMake(@"icon_tabbar_component") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:@"选项0" handler:handler], + [QMUIPopupMenuItem itemWithImage:[UIImageMake(@"icon_tabbar_lab") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:@"选项1" subtitle:@"第二行文字" handler:handler], + ]]; + } + + [items addObject:section1]; + self.menu.itemSections = items; + self.menu.sectionTitles = @[ + @"标题", + @"",// 不需要标题的 section 则用空字符串代替 + ]; + } else { + if (self.useBigData) { + for (NSInteger i = 0; i < 200; i++) { + [items addObject:[QMUIPopupMenuItem itemWithImage:[UIImageMake(@"icon_tabbar_component") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:(self.menu.shouldSelectItemBlock && i == 0) ? @"我不可被选中" : [NSString stringWithFormat:@"选项%@", @(i)] subtitle:i % 2 == 0 ? @"副标题" : nil handler:handler]]; + } + } else { + [items addObjectsFromArray:@[ + [QMUIPopupMenuItem itemWithImage:[UIImageMake(@"icon_tabbar_uikit") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:self.menu.shouldSelectItemBlock ? @"我不可被选中" : @"选项0" handler:handler], + [QMUIPopupMenuItem itemWithImage:[UIImageMake(@"icon_tabbar_component") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:@"选项1" handler:handler], + [QMUIPopupMenuItem itemWithImage:[UIImageMake(@"icon_tabbar_lab") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:@"选项2" subtitle:@"第二行文字" handler:handler], + ]]; + } + self.menu.items = items; + self.menu.sectionTitles = nil; + } + [self updateLayoutAnimated:YES]; +} + +- (void)updateLayoutAnimated:(BOOL)animated { + [UIView qmui_animateWithAnimated:animated duration:.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + [self.view setNeedsLayout]; + [self.view layoutIfNeeded]; + } completion:nil]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + self.scrollView.frame = self.view.bounds; + self.actionButton.qmui_left = self.actionButton.qmui_leftWhenCenterInSuperview; + self.actionButton.qmui_top = 32; + + CGFloat y = self.actionButton.qmui_bottom + 24; + if (self.menu.isShowing) { + y = CGRectGetMaxY([self.menu qmui_convertRect:self.menu.bounds toView:self.scrollView]) + 24; + } + CGSize size = [self.debugViewController contentSizeThatFits:CGSizeMake(320, CGFLOAT_MAX)]; + self.debugViewController.view.frame = CGRectMake(CGFloatGetCenter(CGRectGetWidth(self.view.bounds), 320), y, 320, size.height); + self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetMaxY(self.debugViewController.view.frame) + 32); +} + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDScrollAnimatorViewController.h b/qmuidemo/Modules/Demos/Components/QDScrollAnimatorViewController.h deleted file mode 100644 index d13256a2..00000000 --- a/qmuidemo/Modules/Demos/Components/QDScrollAnimatorViewController.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// QDScrollAnimatorViewController.h -// qmuidemo -// -// Created by QMUI Team on 2018/O/8. -// Copyright © 2018 QMUI Team. All rights reserved. -// - -#import "QDCommonListViewController.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface QDScrollAnimatorViewController : QDCommonListViewController - -@end - -NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Components/QDScrollAnimatorViewController.m b/qmuidemo/Modules/Demos/Components/QDScrollAnimatorViewController.m deleted file mode 100644 index bf9373f0..00000000 --- a/qmuidemo/Modules/Demos/Components/QDScrollAnimatorViewController.m +++ /dev/null @@ -1,31 +0,0 @@ -// -// QDScrollAnimatorViewController.m -// qmuidemo -// -// Created by QMUI Team on 2018/O/8. -// Copyright © 2018 QMUI Team. All rights reserved. -// - -#import "QDScrollAnimatorViewController.h" -#import "QDNavigationBarScrollingAnimatorViewController.h" -#import "QDNavigationBarScrollingSnapAnimatorViewController.h" - -@implementation QDScrollAnimatorViewController - -- (void)initDataSource { - self.dataSource = @[NSStringFromClass([QMUINavigationBarScrollingAnimator class]), - NSStringFromClass([QMUINavigationBarScrollingSnapAnimator class])]; -} - -- (void)didSelectCellWithTitle:(NSString *)title { - UIViewController *viewController = nil; - if ([title isEqualToString:NSStringFromClass([QMUINavigationBarScrollingAnimator class])]) { - viewController = [[QDNavigationBarScrollingAnimatorViewController alloc] init]; - } else if ([title isEqualToString:NSStringFromClass([QMUINavigationBarScrollingSnapAnimator class])]) { - viewController = [[QDNavigationBarScrollingSnapAnimatorViewController alloc] init]; - } - viewController.title = title; - [self.navigationController pushViewController:viewController animated:YES]; -} - -@end diff --git a/qmuidemo/Modules/Demos/Components/QDSheetPresentationViewController.h b/qmuidemo/Modules/Demos/Components/QDSheetPresentationViewController.h new file mode 100644 index 00000000..1ce75e6b --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDSheetPresentationViewController.h @@ -0,0 +1,17 @@ +// +// QDSheetPresentationViewController.h +// qmuidemo +// +// Created by molice on 2024/2/28. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDSheetPresentationViewController : QDCommonViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Components/QDSheetPresentationViewController.m b/qmuidemo/Modules/Demos/Components/QDSheetPresentationViewController.m new file mode 100644 index 00000000..3cff041f --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDSheetPresentationViewController.m @@ -0,0 +1,107 @@ +// +// QDSheetPresentationViewController.m +// qmuidemo +// +// Created by molice on 2024/2/28. +// Copyright © 2024 QMUI Team. All rights reserved. +// + +#import "QDSheetPresentationViewController.h" +#import "QMUIInteractiveDebugger.h" +#import "QDComponentsViewController.h" + +@interface QDSheetPresentationViewController () + +@property(nonatomic, strong) QMUIButton *presentButton; +@property(nonatomic, strong) QMUIInteractiveDebugPanelViewController *asViewController; +@property(nonatomic, strong) QDComponentsViewController *testVc; +@property(nonatomic, assign) BOOL shouldInvalidateLayout; +@end + +@implementation QDSheetPresentationViewController + +- (void)initSubviews { + [super initSubviews]; + + self.testVc = [[QDComponentsViewController alloc] init]; + + self.presentButton = [QDUIHelper generateLightBorderedButton]; + [self.presentButton setTitle:@"点击打开 Sheet 面板" forState:UIControlStateNormal]; + [self.presentButton addTarget:self action:@selector(handlePresentButtonEvent) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.presentButton]; + + self.asViewController = [self generateDebugController]; + [self.view addSubview:self.asViewController.view]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + UIEdgeInsets padding = UIEdgeInsetsMake(32 + self.qmui_navigationBarMaxYInViewCoordinator, 32, 32, 32); + self.presentButton.frame = CGRectSetXY(self.presentButton.frame, CGRectGetMinXHorizontallyCenterInParentRect(self.view.bounds, self.presentButton.frame), padding.top); + CGSize size = [self.asViewController contentSizeThatFits:CGSizeMake(320, CGFLOAT_MAX)]; + self.asViewController.view.frame = CGRectMake(CGFloatGetCenter(CGRectGetWidth(self.view.bounds), 320), CGRectGetMaxY(self.presentButton.frame) + 32, 320, size.height); +} + +- (void)handlePresentButtonEvent { + self.testVc.qmui_sheetPresentation.preferredSheetContentSizeBlock = ^CGSize(QMUISheetPresentation * _Nonnull aSheetPresentation, CGSize aContainerSize) { + return CGSizeMake(aContainerSize.width, aContainerSize.height * 0.6); + }; + QDNavigationController *nav = [[QDNavigationController alloc] qmui_initWithSheetRootViewController:self.testVc]; + [self presentViewController:nav animated:YES completion:nil]; + + if (self.shouldInvalidateLayout) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + self.testVc.qmui_sheetPresentation.preferredSheetContentSizeBlock = ^CGSize(QMUISheetPresentation * _Nonnull aSheetPresentation, CGSize aContainerSize) { + return CGSizeMake(aContainerSize.width, aContainerSize.height * 0.8); + }; + [self.testVc qmui_invalidateSheetPresentationLayout]; + }); + } +} + +- (QMUIInteractiveDebugPanelViewController *)generateDebugController { + __weak __typeof(self)weakSelf = self; + QMUIInteractiveDebugPanelViewController *vc = [QDUIHelper generateDebugViewControllerWithTitle:@"修改面板属性" items:@[ + + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"显示导航栏" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakSelf.testVc.qmui_sheetPresentation.shouldShowNavigationBar; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakSelf.testVc.qmui_sheetPresentation.shouldShowNavigationBar = actionView.on; + }], + + // 跟标准 vc 一样设置标题,浮层会自动关联 + [QMUIInteractiveDebugPanelItem textItemWithTitle:@"标题" valueGetter:^(QMUITextField * _Nonnull actionView) { + actionView.text = weakSelf.testVc.title; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + weakSelf.testVc.title = actionView.text; + }], + [QMUIInteractiveDebugPanelItem numbericItemWithTitle:@"圆角" valueGetter:^(QMUITextField * _Nonnull actionView) { + actionView.text = [NSString stringWithFormat:@"%.0f", weakSelf.testVc.qmui_sheetPresentation.cornerRadius]; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + weakSelf.testVc.qmui_sheetPresentation.cornerRadius = actionView.text.doubleValue; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"modal" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakSelf.testVc.qmui_sheetPresentation.modal; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakSelf.testVc.qmui_sheetPresentation.modal = actionView.on; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"侧滑手势" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakSelf.testVc.qmui_sheetPresentation.supportsSwipeToDismiss; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakSelf.testVc.qmui_sheetPresentation.supportsSwipeToDismiss = actionView.on; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"下拉手势" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakSelf.testVc.qmui_sheetPresentation.supportsPullToDismiss; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakSelf.testVc.qmui_sheetPresentation.supportsPullToDismiss = actionView.on; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"升起后改变高度" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakSelf.shouldInvalidateLayout; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakSelf.shouldInvalidateLayout = actionView.on; + }], + ]]; + return vc; +} + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDStaticTableViewController.m b/qmuidemo/Modules/Demos/Components/QDStaticTableViewController.m index fa4b8927..2ba8ef9d 100644 --- a/qmuidemo/Modules/Demos/Components/QDStaticTableViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDStaticTableViewController.m @@ -79,11 +79,6 @@ - (void)initTableView { d.accessoryValueObject = @YES;// switch 类型的,可以通过传一个 NSNumber 对象给 accessoryValueObject 来指定这个 switch.on 的值 d.accessoryTarget = self; d.accessoryAction = @selector(handleSwitchCellEvent:); - d.cellForRowBlock = ^(UITableView *tableView, __kindof QMUITableViewCell *cell, QMUIStaticTableViewCellData *cellData) { - UISwitch *switchControl = (UISwitch *)cell.accessoryView; - switchControl.onTintColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; - switchControl.tintColor = switchControl.onTintColor; - }; d; })], diff --git a/qmuidemo/Modules/Demos/Components/QDThemeExampleView.h b/qmuidemo/Modules/Demos/Components/QDThemeExampleView.h new file mode 100644 index 00000000..1c2a271a --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDThemeExampleView.h @@ -0,0 +1,17 @@ +// +// QDThemeExampleView.h +// qmuidemo +// +// Created by MoLice on 2019/J/27. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface QDThemeExampleView : UIView + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Components/QDThemeExampleView.m b/qmuidemo/Modules/Demos/Components/QDThemeExampleView.m new file mode 100644 index 00000000..85d3f3ac --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDThemeExampleView.m @@ -0,0 +1,372 @@ +// +// QDThemeExampleView.m +// qmuidemo +// +// Created by MoLice on 2019/J/27. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import "QDThemeExampleView.h" + +@interface QDThemeExampleView () + +@property(nonatomic, assign) CGFloat itemInnerSpacing; +@property(nonatomic, assign) CGFloat itemMarginBottom; +@property(nonatomic, assign) CGFloat barHeight;// bar 高度会受到 safeAreaInsets 的影响,所以搞个固定的高度 + +@property(nonatomic, strong) UILabel *viewLabel; +@property(nonatomic, strong) UIView *view; + +@property(nonatomic, strong) UILabel *textFieldLabel; +@property(nonatomic, strong) UITextField *textField; + +@property(nonatomic, strong) UILabel *labelLabel; +@property(nonatomic, strong) UILabel *label; + +@property(nonatomic, strong) UILabel *textViewLabel; +@property(nonatomic, strong) UITextView *textView; + +@property(nonatomic, strong) UILabel *sliderLabel; +@property(nonatomic, strong) UISlider *slider; + +@property(nonatomic, strong) UILabel *switchLabel; +@property(nonatomic, strong) UISwitch *switchControlOn; +@property(nonatomic, strong) UISwitch *switchControlOff; + +@property(nonatomic, strong) UILabel *imageViewLabel; +@property(nonatomic, strong) UIImageView *imageView; + +@property(nonatomic, strong) UILabel *progressLabel; +@property(nonatomic, strong) UIProgressView *progressView; + +@property(nonatomic, strong) UILabel *layerLabel; +@property(nonatomic, strong) CALayer *exampleLayer; + +@property(nonatomic, strong) UILabel *shapeLayerLabel; +@property(nonatomic, strong) CAShapeLayer *shapeLayer; + +@property(nonatomic, strong) UILabel *gradientLayerLabel; +@property(nonatomic, strong) CAGradientLayer *gradientLayer; + +@property(nonatomic, strong) UILabel *visualEffectLabel; +@property(nonatomic, strong) UIVisualEffectView *visualEffectView; +@property(nonatomic, strong) UIImageView *visualEffectBackendImageView; + +@property(nonatomic, strong) UILabel *navigationBarLabel; +@property(nonatomic, strong) UINavigationBar *navigationBar; + +@property(nonatomic, strong) UILabel *tabBarLabel; +@property(nonatomic, strong) UITabBar *tabBar; + +@property(nonatomic, strong) UILabel *toolbarLabel; +@property(nonatomic, strong) UIToolbar *toolbar; + +@property(nonatomic, strong) UILabel *searchBarLabel; +@property(nonatomic, strong) UISearchBar *searchBar; + +@end + +@implementation QDThemeExampleView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + + self.itemInnerSpacing = 8; + self.itemMarginBottom = 32; + self.barHeight = 44; + + UIFont *font = UIFontMake(14); + UIFont *codeFont = CodeFontMake(font.pointSize); + UIColor *textColor = UIColor.qd_descriptionTextColor; + + self.viewLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.viewLabel.text = @"UIView"; + [self.viewLabel sizeToFit]; + [self addSubview:self.viewLabel]; + + self.view = [[UIView alloc] qmui_initWithSize:CGSizeMake(100, 40)]; + self.view.qmui_borderWidth = 3; + self.view.qmui_borderPosition = QMUIViewBorderPositionTop|QMUIViewBorderPositionLeft|QMUIViewBorderPositionBottom|QMUIViewBorderPositionRight; + self.view.qmui_borderColor = [UIColor.qd_tintColor colorWithAlphaComponent:.5]; + self.view.backgroundColor = [UIColor.qd_tintColor colorWithAlphaComponent:.5]; + [self addSubview:self.view]; + + self.textFieldLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.textFieldLabel.text = @"UITextField"; + [self.textFieldLabel sizeToFit]; + [self addSubview:self.textFieldLabel]; + + self.textField = [[UITextField alloc] qmui_initWithSize:self.view.frame.size]; + self.textField.backgroundColor = [UIColor.qd_tintColor colorWithAlphaComponent:.5]; + self.textField.tintColor = UIColor.qd_tintColor; + self.textField.defaultTextAttributes = @{NSFontAttributeName: UIFontMake(14), + NSForegroundColorAttributeName: UIColor.qd_tintColor}; + self.textField.text = @" example text"; + [self addSubview:self.textField]; + + self.labelLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.labelLabel.text = @"UILabel"; + [self.labelLabel sizeToFit]; + [self addSubview:self.labelLabel]; + + self.label = [[UILabel alloc] init]; + self.label.attributedText = ({ + NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:@"example text..." attributes:@{NSFontAttributeName: UIFontMake(16), + NSForegroundColorAttributeName: UIColor.qd_mainTextColor + }]; + [string addAttribute:NSForegroundColorAttributeName value:UIColor.qd_tintColor range:NSMakeRange(0, @"example".length)]; + string.copy; + }); + self.label.shadowColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject * _Nullable theme) { + return [([identifier isEqualToString:QDThemeIdentifierDark] ? UIColorWhite : UIColorBlack) colorWithAlphaComponent:.5]; + }]; + self.label.shadowOffset = CGSizeMake(1, 1); + [self.label sizeToFit]; + [self addSubview:self.label]; + + self.textViewLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.textViewLabel.text = @"UITextView"; + [self.textViewLabel sizeToFit]; + [self addSubview:self.textViewLabel]; + + self.textView = [[UITextView alloc] qmui_initWithSize:self.textField.frame.size]; + self.textView.backgroundColor = [UIColor.qd_tintColor colorWithAlphaComponent:.5]; + self.textView.tintColor = UIColor.qd_tintColor; + self.textView.typingAttributes = @{NSFontAttributeName: UIFontMake(14), + NSForegroundColorAttributeName: UIColor.qd_tintColor}; + self.textView.text = @"example text"; + [self addSubview:self.textView]; + + self.sliderLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.sliderLabel.text = @"UISlider"; + [self.sliderLabel sizeToFit]; + [self addSubview:self.sliderLabel]; + + self.slider = [[UISlider alloc] init]; + self.slider.minimumTrackTintColor = UIColor.qd_tintColor; + self.slider.maximumTrackTintColor = UIColor.qd_separatorColor; + self.slider.thumbTintColor = self.slider.minimumTrackTintColor; + self.slider.value = .3; + [self.slider sizeToFit]; + [self addSubview:self.slider]; + + self.switchLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.switchLabel.text = @"UISwitch"; + [self.switchLabel sizeToFit]; + [self addSubview:self.switchLabel]; + + self.switchControlOn = [[UISwitch alloc] init]; + self.switchControlOn.on = YES; + [self.switchControlOn sizeToFit]; + self.switchControlOn.onTintColor = UIColor.qd_tintColor; + self.switchControlOn.tintColor = self.switchControlOn.onTintColor; + [self addSubview:self.switchControlOn]; + + self.switchControlOff = [[UISwitch alloc] init]; + [self.switchControlOff sizeToFit]; + self.switchControlOff.onTintColor = self.switchControlOn.onTintColor; + self.switchControlOff.tintColor = self.switchControlOff.onTintColor; + [self addSubview:self.switchControlOff]; + + self.imageViewLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.imageViewLabel.text = @"UIImage"; + [self.imageViewLabel sizeToFit]; + [self addSubview:self.imageViewLabel]; + + UIImage *image = [UIImage qmui_imageWithThemeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, NSString * _Nullable identifier, NSObject * _Nullable theme) { + return [UIImageMake(@"icon_grid_assetsManager") qmui_imageWithTintColor:theme.themeTintColor]; + }]; + self.imageView = [[UIImageView alloc] initWithImage:image]; + [self addSubview:self.imageView]; + + self.progressLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.progressLabel.text = @"UIProgressView"; + [self.progressLabel sizeToFit]; + [self addSubview:self.progressLabel]; + + self.progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault]; + self.progressView.progressTintColor = UIColor.qd_tintColor; + self.progressView.trackTintColor = UIColor.qd_separatorColor; + self.progressView.progress = .3; + [self addSubview:self.progressView]; + + self.layerLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.layerLabel.text = @"CALayer"; + [self.layerLabel sizeToFit]; + [self addSubview:self.layerLabel]; + + self.exampleLayer = CALayer.layer; + self.exampleLayer.cornerRadius = 6; + self.exampleLayer.backgroundColor = UIColor.qd_tintColor.CGColor; + [self.layer addSublayer:self.exampleLayer]; + + self.shapeLayerLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.shapeLayerLabel.text = @"CAShapeLayer"; + [self.shapeLayerLabel sizeToFit]; + [self addSubview:self.shapeLayerLabel]; + + self.shapeLayer = CAShapeLayer.layer; + self.shapeLayer.strokeColor = UIColor.qd_tintColor.CGColor; + self.shapeLayer.lineWidth = 2; + self.shapeLayer.fillColor = [UIColor.qd_tintColor colorWithAlphaComponent:.3].CGColor; + [self.layer addSublayer:self.shapeLayer]; + + self.gradientLayerLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.gradientLayerLabel.text = @"CAGradientLayer"; + [self.gradientLayerLabel sizeToFit]; + [self addSubview:self.gradientLayerLabel]; + + self.gradientLayer = CAGradientLayer.layer; + self.gradientLayer.colors = @[(id)UIColor.qd_tintColor.CGColor, (id)[UIColor.qd_tintColor colorWithAlphaComponent:.3].CGColor]; + self.gradientLayer.startPoint = CGPointMake(0, 1); + self.gradientLayer.endPoint = CGPointMake(0, 0); + [self.layer addSublayer:self.gradientLayer]; + + self.visualEffectLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.visualEffectLabel.text = @"UIVisualEffectView"; + [self.visualEffectLabel sizeToFit]; + [self addSubview:self.visualEffectLabel]; + + self.visualEffectBackendImageView = [[UIImageView alloc] initWithImage:[UIImage qmui_imageWithThemeProvider:^UIImage * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, __kindof NSObject * _Nullable theme) { + return [UIImageMake(@"icon_grid_pieProgressView") qmui_imageWithTintColor:theme.themeTintColor]; + }]]; + [self addSubview:self.visualEffectBackendImageView]; + + self.visualEffectView = [[UIVisualEffectView alloc] init]; + self.visualEffectView.effect = UIVisualEffect.qd_standardBlurEffect; + [self addSubview:self.visualEffectView]; + + self.navigationBarLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.navigationBarLabel.text = @"UINavigationBar"; + [self.navigationBarLabel sizeToFit]; + [self addSubview:self.navigationBarLabel]; + + self.navigationBar = [[UINavigationBar alloc] init]; + [self.navigationBar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault]; + self.navigationBar.barTintColor = UIColor.qd_tintColor; + [self addSubview:self.navigationBar]; + + self.tabBarLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.tabBarLabel.text = @"UITabBar"; + [self.tabBarLabel sizeToFit]; + [self addSubview:self.tabBarLabel]; + + self.tabBar = [[UITabBar alloc] init]; + self.tabBar.backgroundImage = nil; + self.tabBar.barTintColor = UIColor.qd_tintColor; + [self addSubview:self.tabBar]; + + self.toolbarLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.toolbarLabel.text = @"UIToolbar"; + [self.toolbarLabel sizeToFit]; + [self addSubview:self.toolbarLabel]; + + self.toolbar = [[UIToolbar alloc] init]; + [self.toolbar setBackgroundImage:nil forToolbarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault]; + self.toolbar.barTintColor = UIColor.qd_tintColor; + [self addSubview:self.toolbar]; + + self.searchBarLabel = [[UILabel alloc] qmui_initWithFont:codeFont textColor:textColor]; + self.searchBarLabel.text = @"UISearchBar"; + [self.searchBarLabel sizeToFit]; + [self addSubview:self.searchBarLabel]; + + self.searchBar = [[UISearchBar alloc] init]; + self.searchBar.barTintColor = UIColor.qd_tintColor; + self.searchBar.tintColor = UIColor.qd_tintColor; + [self.searchBar sizeToFit]; + [self addSubview:self.searchBar]; + } + return self; +} + +- (CGSize)sizeThatFits:(CGSize)size { + CGFloat height = self.viewLabel.qmui_height + self.itemInnerSpacing + self.view.qmui_height + self.itemMarginBottom; + height += self.labelLabel.qmui_height + self.itemInnerSpacing + self.label.qmui_height + self.itemMarginBottom; + height += self.sliderLabel.qmui_height + self.itemInnerSpacing + self.slider.qmui_height + self.itemMarginBottom; + height += self.switchLabel.qmui_height + self.itemInnerSpacing + self.switchControlOn.qmui_height + self.itemMarginBottom; + height += self.progressLabel.qmui_height + self.itemInnerSpacing + self.progressView.qmui_height + self.itemMarginBottom; + height += self.visualEffectLabel.qmui_height + self.itemInnerSpacing + self.barHeight + self.itemMarginBottom; + height += self.navigationBarLabel.qmui_height + self.itemInnerSpacing + self.barHeight + self.itemMarginBottom; + height += self.tabBarLabel.qmui_height + self.itemInnerSpacing + self.barHeight + self.itemMarginBottom; + height += self.toolbarLabel.qmui_height + self.itemInnerSpacing + self.barHeight + self.itemMarginBottom; + height += self.searchBarLabel.qmui_height + self.itemInnerSpacing + self.searchBar.qmui_height + self.itemMarginBottom; + size.height = height; + return size; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + self.view.qmui_top = self.viewLabel.qmui_bottom + self.itemInnerSpacing; + + self.textField.qmui_top = self.view.qmui_top; + self.textField.qmui_right = self.qmui_width; + self.textFieldLabel.qmui_left = self.textField.qmui_left; + + self.labelLabel.qmui_top = self.view.qmui_bottom + self.itemMarginBottom; + self.label.qmui_top = self.labelLabel.qmui_bottom + self.itemInnerSpacing; + + self.textView.qmui_top = self.label.qmui_top; + self.textView.qmui_right = self.qmui_width; + self.textViewLabel.qmui_left = self.textView.qmui_left; + self.textViewLabel.qmui_top = self.labelLabel.qmui_top; + + self.sliderLabel.qmui_top = self.label.qmui_bottom + self.itemMarginBottom; + self.slider.qmui_top = self.sliderLabel.qmui_bottom + self.itemInnerSpacing; + self.slider.qmui_width = self.qmui_width * 2 / 3; + + self.switchLabel.qmui_top = self.slider.qmui_bottom + self.itemMarginBottom; + self.switchControlOn.qmui_top = self.switchLabel.qmui_bottom + self.itemInnerSpacing; + self.switchControlOff.qmui_top = self.switchControlOn.qmui_top; + self.switchControlOff.qmui_left = self.switchControlOn.qmui_right + 36; + + self.imageViewLabel.qmui_left = self.textViewLabel.qmui_left; + self.imageViewLabel.qmui_top = self.switchLabel.qmui_top; + self.imageView.qmui_top = self.imageViewLabel.qmui_bottom + self.itemInnerSpacing; + self.imageView.qmui_left = self.imageViewLabel.qmui_left; + + self.progressLabel.qmui_top = self.switchControlOn.qmui_bottom + self.itemMarginBottom; + self.progressView.qmui_top = self.progressLabel.qmui_bottom + self.itemInnerSpacing; + + CGFloat layerWidth = (CGRectGetWidth(self.bounds) - 16 * 2) / 3; + + self.layerLabel.qmui_top = self.progressView.qmui_bottom + self.itemMarginBottom; + self.exampleLayer.frame = CGRectMake(self.layerLabel.qmui_left, self.layerLabel.qmui_bottom + self.itemInnerSpacing, layerWidth, layerWidth - 20); + + self.shapeLayerLabel.qmui_top = self.layerLabel.qmui_top; + self.shapeLayerLabel.qmui_left = CGRectGetMaxX(self.exampleLayer.frame) + 16; + self.shapeLayer.frame = CGRectSetX(self.exampleLayer.frame, self.shapeLayerLabel.qmui_left); + self.shapeLayer.path = [UIBezierPath bezierPathWithOvalInRect:self.shapeLayer.bounds].CGPath; + + self.gradientLayerLabel.qmui_top = self.layerLabel.qmui_top; + self.gradientLayerLabel.qmui_left = CGRectGetMaxX(self.shapeLayer.frame) + 16; + self.gradientLayer.frame = CGRectSetX(self.exampleLayer.frame, self.gradientLayerLabel.qmui_left); + + self.visualEffectLabel.qmui_top = CGRectGetMaxY(self.exampleLayer.frame) + self.itemMarginBottom; + self.visualEffectView.qmui_top = self.visualEffectLabel.qmui_bottom + self.itemInnerSpacing; + self.visualEffectView.qmui_width = self.qmui_width; + self.visualEffectView.qmui_height = self.barHeight; + self.visualEffectBackendImageView.center = self.visualEffectView.center; + + self.navigationBarLabel.qmui_top = self.visualEffectView.qmui_bottom + self.itemMarginBottom; + self.navigationBar.qmui_top = self.navigationBarLabel.qmui_bottom + self.itemInnerSpacing; + self.navigationBar.qmui_width = self.qmui_width; + self.navigationBar.qmui_height = self.barHeight; + + self.tabBarLabel.qmui_top = self.navigationBar.qmui_bottom + self.itemMarginBottom; + self.tabBar.qmui_top = self.tabBarLabel.qmui_bottom + self.itemInnerSpacing; + self.tabBar.qmui_width = self.qmui_width; + self.tabBar.qmui_height = self.barHeight; + + self.toolbarLabel.qmui_top = self.tabBar.qmui_bottom + self.itemMarginBottom; + self.toolbar.qmui_top = self.toolbarLabel.qmui_bottom + self.itemInnerSpacing; + self.toolbar.qmui_width = self.qmui_width; + self.toolbar.qmui_height = self.barHeight; + + self.searchBarLabel.qmui_top = self.toolbar.qmui_bottom + self.itemMarginBottom; + self.searchBar.qmui_top = self.searchBarLabel.qmui_bottom + self.itemInnerSpacing; + self.searchBar.qmui_width = self.qmui_width; +} + +@end diff --git a/qmuidemo/Modules/Demos/Lab/QDThemeViewController.h b/qmuidemo/Modules/Demos/Components/QDThemeViewController.h similarity index 100% rename from qmuidemo/Modules/Demos/Lab/QDThemeViewController.h rename to qmuidemo/Modules/Demos/Components/QDThemeViewController.h diff --git a/qmuidemo/Modules/Demos/Components/QDThemeViewController.m b/qmuidemo/Modules/Demos/Components/QDThemeViewController.m new file mode 100644 index 00000000..fef2082b --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDThemeViewController.m @@ -0,0 +1,226 @@ +// +// QDThemeViewController.m +// qmuidemo +// +// Created by QMUI Team on 2017/5/10. +// Copyright © 2017年 QMUI Team. All rights reserved. +// + +#import "QDThemeViewController.h" +#import "QMUIConfigurationTemplate.h" +#import "QMUIConfigurationTemplateGrapefruit.h" +#import "QMUIConfigurationTemplateGrass.h" +#import "QMUIConfigurationTemplatePinkRose.h" +#import "QMUIConfigurationTemplateDark.h" +#import "QDThemeExampleView.h" + +@interface QDThemeButton : QMUIButton + +@property(nonatomic, strong) UIColor *themeColor; +@property(nonatomic, copy) NSString *themeName; +@end + +@interface QDThemeViewController () + +@property(nonatomic, strong) NSArray *classes; +@property(nonatomic, strong) UIScrollView *scrollView; +@property(nonatomic, strong) QMUIFloatLayoutView *buttonContainers; +@property(nonatomic, strong) NSMutableArray *themeButtons; +@property(nonatomic, strong) UISwitch *respondsSystemStyleSwitch; +@property(nonatomic, strong) UILabel *respondsSystemStyleLabel; +@property(nonatomic, strong) CALayer *separatorLayer; +@property(nonatomic, strong) QDThemeExampleView *exampleView; +@property(nonatomic, strong) QMUIKeyboardManager *keyboardManager; +@end + +@implementation QDThemeViewController + +- (void)didInitialize { + [super didInitialize]; + + self.classes = @[ + QMUIConfigurationTemplate.class, + QMUIConfigurationTemplateGrapefruit.class, + QMUIConfigurationTemplateGrass.class, + QMUIConfigurationTemplatePinkRose.class, + QMUIConfigurationTemplateDark.class]; + [self.classes enumerateObjectsUsingBlock:^(Class _Nonnull class, NSUInteger idx, BOOL * _Nonnull stop) { + BOOL hasInstance = NO; + for (NSObject *theme in QMUIThemeManagerCenter.defaultThemeManager.themes) { + if ([theme isKindOfClass:class]) { + hasInstance = YES; + break; + } + } + if (!hasInstance) { + NSObject *theme = [class new]; + [QMUIThemeManagerCenter.defaultThemeManager addThemeIdentifier:theme.themeName theme:theme]; + } + }]; + + self.themeButtons = [[NSMutableArray alloc] init]; + + self.keyboardManager = [[QMUIKeyboardManager alloc] initWithDelegate:self]; +} + +- (void)initSubviews { + [super initSubviews]; + + self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; + [self.view addSubview:self.scrollView]; + + self.buttonContainers = [[QMUIFloatLayoutView alloc] init]; + [self.scrollView addSubview:self.buttonContainers]; + + [self.classes enumerateObjectsUsingBlock:^(Class _Nonnull class, NSUInteger idx, BOOL * _Nonnull stop) { + for (NSObject *theme in QMUIThemeManagerCenter.defaultThemeManager.themes) { + if ([NSStringFromClass(theme.class) isEqualToString:NSStringFromClass(class)]) { + NSString *identifier = [QMUIThemeManagerCenter.defaultThemeManager identifierForTheme:theme]; + BOOL isCurrentTheme = [QMUIThemeManagerCenter.defaultThemeManager.currentThemeIdentifier isEqual:identifier]; + QDThemeButton *themeButton = [[QDThemeButton alloc] init]; + themeButton.themeColor = [theme.themeName isEqualToString:QDThemeIdentifierDark] ? UIColorBlack : theme.themeTintColor; + themeButton.themeName = theme.themeName; + themeButton.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; + themeButton.selected = isCurrentTheme; + [themeButton addTarget:self action:@selector(handleThemeButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.buttonContainers addSubview:themeButton]; + [self.themeButtons addObject:themeButton]; + break; + } + } + }]; + + self.respondsSystemStyleSwitch = [[UISwitch alloc] init]; + self.respondsSystemStyleSwitch.on = QMUIThemeManagerCenter.defaultThemeManager.respondsSystemStyleAutomatically; + [self.respondsSystemStyleSwitch addTarget:self action:@selector(handleSwitchEvent:) forControlEvents:UIControlEventValueChanged]; + [self.scrollView addSubview:self.respondsSystemStyleSwitch]; + + self.respondsSystemStyleLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; + self.respondsSystemStyleLabel.text = @"自动响应 iOS 13 系统样式(Dark Mode)"; + [self.respondsSystemStyleLabel sizeToFit]; + [self.scrollView addSubview:self.respondsSystemStyleLabel]; + + self.separatorLayer = [CALayer layer]; + self.separatorLayer.backgroundColor = UIColor.qd_tintColor.CGColor; + [self.scrollView.layer addSublayer:self.separatorLayer]; + + self.exampleView = [[QDThemeExampleView alloc] init]; + [self.scrollView addSubview:self.exampleView]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + + self.scrollView.frame = self.view.bounds; + + UIEdgeInsets paddings = UIEdgeInsetsMake(24, 24 + self.scrollView.safeAreaInsets.left, 24 + self.scrollView.safeAreaInsets.bottom, 24 + self.scrollView.safeAreaInsets.right); + self.buttonContainers.itemMargins = UIEdgeInsetsMake(0, 0, 8, 8); + // 窄屏幕一行两个,宽屏幕单行展示完整 + CGFloat buttonWidth = CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(paddings); + if (buttonWidth > [QMUIHelper screenSizeFor65Inch].width) { + buttonWidth = ((buttonWidth + self.buttonContainers.itemMargins.right) / self.buttonContainers.subviews.count) - self.buttonContainers.itemMargins.right; + } else { + buttonWidth = (buttonWidth - self.buttonContainers.itemMargins.right) / 2; + } + buttonWidth = floor(buttonWidth); + [self.themeButtons enumerateObjectsUsingBlock:^(QDThemeButton * _Nonnull button, NSUInteger idx, BOOL * _Nonnull stop) { + button.frame = CGRectSetSize(button.frame, CGSizeMake(buttonWidth, 32)); + }]; + self.buttonContainers.frame = CGRectMake(paddings.left, paddings.top, CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(paddings), QMUIViewSelfSizingHeight); + + self.respondsSystemStyleSwitch.qmui_left = self.buttonContainers.qmui_left; + self.respondsSystemStyleSwitch.qmui_top = self.buttonContainers.qmui_bottom + 18; + self.respondsSystemStyleLabel.qmui_left = self.respondsSystemStyleSwitch.qmui_right + 12; + self.respondsSystemStyleLabel.qmui_top = self.respondsSystemStyleSwitch.qmui_top + CGFloatGetCenter(self.respondsSystemStyleSwitch.qmui_height, self.respondsSystemStyleLabel.qmui_height); + + self.separatorLayer.frame = CGRectMake(paddings.left, CGRectGetMaxY(self.respondsSystemStyleSwitch.frame) + 18, CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(paddings), PixelOne); + + self.exampleView.frame = CGRectMake(paddings.left, CGRectGetMaxY(self.separatorLayer.frame) + 24, CGRectGetWidth(self.separatorLayer.frame), QMUIViewSelfSizingHeight); + + self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.scrollView.bounds), CGRectGetMaxY(self.exampleView.frame) + paddings.bottom); +} + +- (void)handleSwitchEvent:(UISwitch *)switchControl { + QMUIThemeManagerCenter.defaultThemeManager.respondsSystemStyleAutomatically = switchControl.on; +} + +- (void)handleThemeButtonEvent:(QDThemeButton *)themeButton { + QMUIThemeManagerCenter.defaultThemeManager.currentThemeIdentifier = themeButton.currentTitle; +} + +- (void)qmui_themeDidChangeByManager:(QMUIThemeManager *)manager identifier:(NSString *)identifier theme:(__kindof NSObject *)theme { + [super qmui_themeDidChangeByManager:manager identifier:identifier theme:theme]; + [self.themeButtons enumerateObjectsUsingBlock:^(QDThemeButton * _Nonnull button, NSUInteger idx, BOOL * _Nonnull stop) { + button.selected = [button.currentTitle isEqualToString:identifier]; + }]; +} + +- (BOOL)shouldHideKeyboardWhenTouchInView:(UIView *)view { + return YES; +} + +#pragma mark - + +- (void)keyboardWillChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + CGFloat marginToKeyboard = 16; + UIView *view = (UIView *)keyboardUserInfo.targetResponder; + CGRect rectInView = [view convertRect:view.bounds toView:self.view]; + CGFloat keyboardHeight = [keyboardUserInfo heightInView:self.view]; + if (keyboardHeight <= 0) { + // hide + if (self.scrollView.contentOffset.y + CGRectGetHeight(self.scrollView.bounds) > self.scrollView.contentSize.height) { + [UIView animateWithDuration:keyboardUserInfo.animationDuration delay:0 options:keyboardUserInfo.animationOptions animations:^{ + [self.scrollView qmui_scrollToBottom]; + } completion:nil]; + } + } else { + // show + CGFloat delta = CGRectGetHeight(self.view.bounds) - keyboardHeight - marginToKeyboard - CGRectGetMaxY(rectInView); + if (delta < 0) { + [UIView animateWithDuration:keyboardUserInfo.animationDuration delay:0 options:keyboardUserInfo.animationOptions animations:^{ + [self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, self.scrollView.contentOffset.y - delta)]; + } completion:nil]; + } + } + + self.scrollView.contentInset = UIEdgeInsetsSetBottom(self.scrollView.contentInset, keyboardHeight); + self.scrollView.scrollIndicatorInsets = self.scrollView.contentInset; +} + +@end + +@implementation QDThemeButton + +- (void)updateStyle { + self.backgroundColor = self.selected ? self.themeColor : nil; + if ([self.themeName isEqualToString:QDThemeIdentifierDark] && self.selected) { + self.backgroundColor = [UIColorWhite colorWithAlphaComponent:.7]; + } + self.layer.borderWidth = self.selected ? 0 : 1; + self.layer.borderColor = self.themeColor.CGColor; + [self setTitleColor:self.themeColor forState:UIControlStateNormal]; + [self setTitleColor:UIColorWhite forState:UIControlStateSelected]; + self.titleLabel.font = self.selected ? UIFontBoldMake(12) : UIFontMake(12); + self.cornerRadius = 4; +} + +- (void)setThemeColor:(UIColor *)themeColor { + _themeColor = themeColor; + [self updateStyle]; +} + +- (void)setThemeName:(NSString *)themeName { + _themeName = themeName; + [self setTitle:themeName forState:UIControlStateNormal]; +} + +- (void)setSelected:(BOOL)selected { + [super setSelected:selected]; + [self updateStyle]; +} + +- (CGSize)sizeThatFits:(CGSize)size { + return self.frame.size; +} + +@end diff --git a/qmuidemo/Modules/Demos/Lab/Animation/QDAnimationCurvesViewController.h b/qmuidemo/Modules/Demos/Lab/Animation/QDAnimationCurvesViewController.h new file mode 100644 index 00000000..1e8e33d8 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/Animation/QDAnimationCurvesViewController.h @@ -0,0 +1,17 @@ +// +// QDAnimationCurvesViewController.h +// qmuidemo +// +// Created by molice on 2021/11/16. +// Copyright © 2021 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDAnimationCurvesViewController : QDCommonViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Lab/Animation/QDAnimationCurvesViewController.m b/qmuidemo/Modules/Demos/Lab/Animation/QDAnimationCurvesViewController.m new file mode 100644 index 00000000..6bfee273 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/Animation/QDAnimationCurvesViewController.m @@ -0,0 +1,306 @@ +// +// QDAnimationCurvesViewController.m +// qmuidemo +// +// Created by molice on 2021/11/16. +// Copyright © 2021 QMUI Team. All rights reserved. +// + +#import "QDAnimationCurvesViewController.h" + +@interface QDAnimationCurvesCell : UICollectionViewCell +@property(nonatomic, strong, readonly) CAShapeLayer *shapeLayer; +@property(nonatomic, strong, readonly) UILabel *nameLabel; +@property(nonatomic, strong) UIBezierPath *path; +@property(nonatomic, assign) BOOL pathChanged; +@end + +@interface QDAnimationCurvesViewController () + +@property(nonatomic, strong) NSArray *> *paths; +@property(nonatomic, strong) UICollectionView *collectionView; +@property(nonatomic, strong) UICollectionViewFlowLayout *collectionLayout; +@property(nonatomic, strong) UILongPressGestureRecognizer *longPressGesture; +@property(nonatomic, strong) QDAnimationCurvesCell *longPressedCell; +@property(nonatomic, strong) CAShapeLayer *pinnedShapeLayer; +@end + +@implementation QDAnimationCurvesViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.paths = @[ + @{ + @"System Pop": [self.class bezierPathWithX:[self.class systemXs] y:[self.class systemYs]], + }, + @{ + @"fastLinearToSlowEaseIn": [UIBezierPath qmui_bezierPathWithMediaTimingFunction:[CAMediaTimingFunction functionWithControlPoints:0.18 :1.0 :0.04 :1.0]], + }, + @{ + @"fastLinearToSlowEaseIn2": [UIBezierPath qmui_bezierPathWithMediaTimingFunction:[CAMediaTimingFunction functionWithControlPoints:0.2 :1.0 :0.04 :0.92]], + }, + @{ + @"EaseIn": [UIBezierPath qmui_bezierPathWithMediaTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]], + }, + @{ + @"EaseOut": [UIBezierPath qmui_bezierPathWithMediaTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]], + }, + @{ + @"EaseInOut": [UIBezierPath qmui_bezierPathWithMediaTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]], + }, + ]; + + self.collectionLayout = [[UICollectionViewFlowLayout alloc] init]; + self.collectionLayout.itemSize = CGSizeMake(200, 223); + self.collectionLayout.scrollDirection = UICollectionViewScrollDirectionVertical; + self.collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.collectionLayout]; + self.collectionView.backgroundColor = TableViewInsetGroupedBackgroundColor; + self.collectionView.dataSource = self; + self.collectionView.delegate = self; + [self.collectionView registerClass:QDAnimationCurvesCell.class forCellWithReuseIdentifier:@"cell"]; + [self.view addSubview:self.collectionView]; + + self.longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; + self.longPressGesture.minimumPressDuration = 0.3; + [self.collectionView addGestureRecognizer:self.longPressGesture]; +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + self.titleView.style = QMUINavigationTitleViewStyleSubTitleVertical; + self.titleView.subtitle = @"长按曲线可悬停对比"; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + self.collectionView.frame = self.view.bounds; + if (!self.pinnedShapeLayer.animationKeys.count) {// 动画过程中屏蔽布局,避免冲突 + self.pinnedShapeLayer.position = CGPointMake(CGRectGetWidth(self.view.bounds) / 2, self.qmui_navigationBarMaxYInViewCoordinator + CGRectGetHeight(self.pinnedShapeLayer.bounds) / 2); + } +} + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { + return 1; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + return self.paths.count; +} + +- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + QDAnimationCurvesCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath]; + NSDictionary *data = self.paths[indexPath.item]; + cell.nameLabel.text = data.allKeys.firstObject; + cell.path = data.allValues.firstObject; + return cell; +} + +- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewFlowLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { + CGFloat horizontal = (CGRectGetWidth(collectionView.bounds) - collectionViewLayout.itemSize.width) / 2; + return UIEdgeInsetsMake(24, horizontal, 24, horizontal); +} + +- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)gesture { + if (gesture.state == UIGestureRecognizerStateBegan) { + CGPoint point = [gesture locationInView:self.collectionView]; + NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:point]; + if (!indexPath) return; + + QDAnimationCurvesCell *cell = (QDAnimationCurvesCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; + self.longPressedCell = cell; + [UIView animateWithDuration:.25 delay:0 options:QMUIViewAnimationOptionsCurveIn animations:^{ + cell.transform = CGAffineTransformMakeScale(.95, .95); + } completion:nil]; + return; + } + + if (gesture.state == UIGestureRecognizerStateEnded) { + if (!self.longPressedCell) return; + + if (!self.pinnedShapeLayer) { + self.pinnedShapeLayer = [self.class generateShapeLayer]; + self.pinnedShapeLayer.opacity = .5; + [self.view.layer addSublayer:self.pinnedShapeLayer]; + } + self.pinnedShapeLayer.bounds = self.longPressedCell.shapeLayer.bounds; + self.pinnedShapeLayer.path = self.longPressedCell.shapeLayer.path; + self.pinnedShapeLayer.affineTransform = self.longPressedCell.transform; + self.pinnedShapeLayer.position = [self.longPressedCell.contentView convertPoint:self.longPressedCell.shapeLayer.position toView:self.view]; + + CABasicAnimation *positionAnimation = [CABasicAnimation animationWithKeyPath:@"position"]; + positionAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(CGRectGetWidth(self.view.bounds) / 2, self.qmui_navigationBarMaxYInViewCoordinator + CGRectGetHeight(self.pinnedShapeLayer.bounds) / 2)]; + CABasicAnimation *transformAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; + transformAnimation.toValue = @1; + CAAnimationGroup *animation = [[CAAnimationGroup alloc] init]; + animation.animations = @[positionAnimation, transformAnimation]; + animation.duration = .3; + animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + __weak __typeof(self)weakSelf = self; + animation.qmui_animationDidStopBlock = ^(__kindof CAAnimation *aAnimation, BOOL finished) { + weakSelf.pinnedShapeLayer.affineTransform = CGAffineTransformIdentity; + [weakSelf.view setNeedsLayout]; + [weakSelf.view layoutIfNeeded]; + }; + [self.pinnedShapeLayer addAnimation:animation forKey:@"pinned"]; + } + + if (self.longPressedCell) { + [UIView animateWithDuration:.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + self.longPressedCell.transform = CGAffineTransformIdentity; + } completion:nil]; + self.longPressedCell = nil; + } +} + ++ (CAShapeLayer *)generateShapeLayer { + CAShapeLayer *shapeLayer = [CAShapeLayer layer]; + [shapeLayer qmui_removeDefaultAnimations]; + shapeLayer.geometryFlipped = YES; + shapeLayer.lineWidth = 2; + shapeLayer.strokeColor = UIColor.qd_tintColor.CGColor; + shapeLayer.fillColor = UIColor.clearColor.CGColor; + shapeLayer.backgroundColor = [UIColor.qd_tintColor colorWithAlphaComponent:.2].CGColor; + return shapeLayer; +} + ++ (NSArray *)systemXs { + static NSArray *x = nil; + if (!x) { + x = @[ + @0, + @0.024503979636086443, + @0.057266700762091075, + @0.09046306901600994, + @0.1219015134868146, + @0.15445032795779426, + @0.18752807524847204, + @0.220843064465632, + @0.2526315380082957, + @0.2857053960870639, + @0.3176338812584712, + @0.35093720283990254, + @0.383530743247842, + @0.41477083791125985, + @0.4470046262175664, + @0.47952621620517927, + @0.511586934581511, + @0.5441474166882194, + @0.5774118461505553, + @0.6089086288000031, + @0.6433184311696999, + @0.6750407881099013, + @0.7068564861359319, + @0.7397183821656299, + @0.7716857594561326, + @0.803793148375379, + @0.8381971169272114, + @0.8705981413456284, + @0.9013190262191221, + @0.935582983142211, + @0.9660316231820365, + @1, + ]; + } + return x; +} + ++ (NSArray *)systemYs { + static NSArray *y = nil; + if (!y) { + y = @[ + @0.0002399999999999333, + @0.026746666666666592, + @0.10834666666666666, + @0.21632, + @0.32434666666666667, + @0.43176, + @0.5307733333333333, + @0.6180533333333333, + @0.6891200000000001, + @0.7511733333333334, + @0.8005599999999999, + @0.8426666666666667, + @0.87584, + @0.9014933333333334, + @0.9226933333333333, + @0.9396533333333333, + @0.95288, + @0.9634666666666667, + @0.9718933333333333, + @0.9781066666666667, + @0.9833866666666666, + @0.9871466666666667, + @0.9900533333333332, + @0.9924, + @0.99416, + @0.99552, + @0.99664, + @0.9974400000000001, + @0.998, + @0.9985066666666667, + @0.9988533333333333, + @1, + ]; + } + return y; +} + ++ (UIBezierPath *)bezierPathWithX:(NSArray *)xs y:(NSArray *)ys { + NSAssert(xs.count == ys.count, @""); + UIBezierPath *path = [[UIBezierPath alloc] init]; + [xs enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + CGFloat x = obj.qmui_CGFloatValue; + CGFloat y = ys[idx].qmui_CGFloatValue; + CGPoint point = CGPointMake(x, y); + if (idx == 0) { + [path moveToPoint:point]; + } else { + [path addLineToPoint:point]; + } + }]; + return path; +} + +@end + +@implementation QDAnimationCurvesCell + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + _nameLabel = [[UILabel alloc] qmui_initWithFont:UIFontLightMake(12) textColor:UIColor.qd_mainTextColor]; + self.nameLabel.adjustsFontSizeToFitWidth = YES; + self.nameLabel.textAlignment = NSTextAlignmentCenter; + [self.contentView addSubview:self.nameLabel]; + + _shapeLayer = [QDAnimationCurvesViewController generateShapeLayer]; + [self.contentView.layer addSublayer:self.shapeLayer]; + + self.backgroundColor = TableViewInsetGroupedCellBackgroundColor; + self.layer.cornerRadius = TableViewInsetGroupedCornerRadius; + self.layer.masksToBounds = YES; + } + return self; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + CGFloat shapeLayerWidth = CGRectGetWidth(self.contentView.bounds) - 16 * 2; + if (CGRectGetWidth(self.shapeLayer.frame) != shapeLayerWidth || self.pathChanged) { + UIBezierPath *path = self.path.copy; + [path applyTransform:CGAffineTransformMakeScale(shapeLayerWidth, shapeLayerWidth)]; + self.shapeLayer.path = path.CGPath; + } + self.shapeLayer.frame = CGRectMake(16, 16, shapeLayerWidth, shapeLayerWidth); + self.nameLabel.frame = CGRectMake(16, CGRectGetMaxY(self.shapeLayer.frame) + 8, shapeLayerWidth, QMUIViewSelfSizingHeight); +} + +- (void)setPath:(UIBezierPath *)path { + self.pathChanged = _path != path; + _path = path; + if (self.pathChanged) { + [self setNeedsLayout]; + } +} + +@end diff --git a/qmuidemo/Modules/Demos/Lab/Animation/QDAnimationViewController.h b/qmuidemo/Modules/Demos/Lab/Animation/QDAnimationViewController.h deleted file mode 100644 index ad6677b3..00000000 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDAnimationViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDAnimationViewController.h -// qmui -// -// Created by QMUI Team on 14-9-23. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QDCommonListViewController.h" - -@interface QDAnimationViewController : QDCommonListViewController - -@end diff --git a/qmuidemo/Modules/Demos/Lab/Animation/QDAnimationViewController.m b/qmuidemo/Modules/Demos/Lab/Animation/QDAnimationViewController.m deleted file mode 100644 index 09b7c0fb..00000000 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDAnimationViewController.m +++ /dev/null @@ -1,43 +0,0 @@ -// -// QDAnimationViewController.m -// qmui -// -// Created by QMUI Team on 14-9-23. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QDAnimationViewController.h" -#import "QDAllAnimationViewController.h" -#import "QDCAShapeLoadingViewController.h" -#import "QDReplicatorLayerViewController.h" -#import "QDRippleAnimationViewController.h" - -@implementation QDAnimationViewController - -- (void)initDataSource { - [super initDataSource]; - self.dataSource = @[@"Loading", - @"Loading With CAShapeLayer", - @"Animation For CAReplicatorLayer", - @"水波纹"]; -} - -- (void)didSelectCellWithTitle:(NSString *)title { - UIViewController *viewController = nil; - if ([title isEqualToString:@"Loading"]) { - viewController = [[QDAllAnimationViewController alloc] init]; - } - else if ([title isEqualToString:@"Loading With CAShapeLayer"]) { - viewController = [[QDCAShapeLoadingViewController alloc] init]; - } - else if ([title isEqualToString:@"Animation For CAReplicatorLayer"]) { - viewController = [[QDReplicatorLayerViewController alloc] init]; - } - else if ([title isEqualToString:@"水波纹"]) { - viewController = [[QDRippleAnimationViewController alloc] init]; - } - viewController.title = title; - [self.navigationController pushViewController:viewController animated:YES]; -} - -@end diff --git a/qmuidemo/Modules/Demos/Lab/Animation/QDRippleAnimationViewController.m b/qmuidemo/Modules/Demos/Lab/Animation/QDRippleAnimationViewController.m index 1f8b3aba..930d3e63 100644 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDRippleAnimationViewController.m +++ b/qmuidemo/Modules/Demos/Lab/Animation/QDRippleAnimationViewController.m @@ -55,7 +55,7 @@ - (void)initSubviews { _textLabel = [[UILabel alloc] init]; _textLabel.numberOfLines = 0; _textLabel.textAlignment = NSTextAlignmentCenter; - _textLabel.textColor = UIColorGray4; + _textLabel.textColor = UIColor.qd_descriptionTextColor; _textLabel.font = UIFontMake(16); _textLabel.text = @"第一个动画使用CAAnimationGroup来实现,第二个动画使用CAReplicatorLayer来实现。"; [_scrollView addSubview:_textLabel]; diff --git a/qmuidemo/Modules/Demos/Lab/QDAllSystemFontsViewController.m b/qmuidemo/Modules/Demos/Lab/QDAllSystemFontsViewController.m index d7af7299..53e8aa1a 100644 --- a/qmuidemo/Modules/Demos/Lab/QDAllSystemFontsViewController.m +++ b/qmuidemo/Modules/Demos/Lab/QDAllSystemFontsViewController.m @@ -11,15 +11,18 @@ @interface QDAllSystemFontsViewController () @property(nonatomic, strong) NSMutableArray *allFonts; +@property(nonatomic, strong) NSMutableArray *searchResultFonts; @end @implementation QDAllSystemFontsViewController - (instancetype)initWithStyle:(UITableViewStyle)style { if (self = [super initWithStyle:style]) { + self.shouldShowSearchBar = YES; self.allFonts = [[NSMutableArray alloc] init]; - dispatch_async(dispatch_get_global_queue(0, 0), ^{ - for (NSString *familyName in [UIFont familyNames]) { + self.searchResultFonts = [[NSMutableArray alloc] init]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + for (NSString *familyName in [UIFont familyNames]) {// 注意,familyNames 获取到的字体大全里不包含系统默认字体(iOS 13 是 .SFUI,iOS 12 及以前是 .SFUIText) for (NSString *fontName in [UIFont fontNamesForFamilyName:familyName]) { [self.allFonts addObject:[UIFont fontWithName:fontName size:16]]; } @@ -35,11 +38,13 @@ - (instancetype)initWithStyle:(UITableViewStyle)style { } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return self.allFonts.count; + NSArray *fonts = tableView == self.tableView ? self.allFonts : self.searchResultFonts; + return fonts.count; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - NSString *fontName = self.allFonts[indexPath.row].fontName; + NSArray *fonts = tableView == self.tableView ? self.allFonts : self.searchResultFonts; + NSString *fontName = fonts[indexPath.row].fontName; if ([fontName containsString:@"Zapfino"]) { // 这个字体很飘逸,不够高是显示不全的 return TableViewCellNormalHeight + 60; @@ -48,16 +53,17 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + NSArray *fonts = tableView == self.tableView ? self.allFonts : self.searchResultFonts; static NSString *identifier = @"cell"; QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; if (!cell) { cell = [[QMUITableViewCell alloc] initForTableView:tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier]; cell.selectionStyle = UITableViewCellSelectionStyleNone; - cell.textLabel.textColor = UIColorBlack; - cell.detailTextLabel.textColor = UIColorGray3; + cell.textLabel.textColor = UIColor.qd_mainTextColor; + cell.detailTextLabel.textColor = UIColor.qd_descriptionTextColor; } - UIFont *font = self.allFonts[indexPath.row]; + UIFont *font = fonts[indexPath.row]; cell.textLabel.font = font; cell.textLabel.text = [NSString stringWithFormat:@"%@ %@", @(indexPath.row + 1), font.fontName]; cell.detailTextLabel.font = font; @@ -66,4 +72,14 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N return cell; } +- (void)searchController:(QMUISearchController *)searchController updateResultsForSearchString:(NSString *)searchString { + [self.searchResultFonts removeAllObjects]; + for (UIFont *font in self.allFonts) { + if ([font.fontName.lowercaseString containsString:searchString.lowercaseString]) { + [self.searchResultFonts addObject:font]; + } + } + [searchController.tableView reloadData]; +} + @end diff --git a/qmuidemo/Modules/Demos/Lab/QDBackBarButtonViewController.h b/qmuidemo/Modules/Demos/Lab/QDBackBarButtonViewController.h new file mode 100644 index 00000000..25f7557a --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QDBackBarButtonViewController.h @@ -0,0 +1,17 @@ +// +// QDBackBarButtonViewController.h +// qmuidemo +// +// Created by MoLice on 2020/12/7. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDCommonTableViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDBackBarButtonViewController : QDCommonTableViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Lab/QDBackBarButtonViewController.m b/qmuidemo/Modules/Demos/Lab/QDBackBarButtonViewController.m new file mode 100644 index 00000000..45231b77 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QDBackBarButtonViewController.m @@ -0,0 +1,228 @@ +// +// QDBackBarButtonViewController.m +// qmuidemo +// +// Created by MoLice on 2020/12/7. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDBackBarButtonViewController.h" +#import "QMUIBackBarButton.h" + +/// 消息列表子界面的返回按钮要用圆形未读数来显示。 +/// 必须继承自 QMUINavigationButton,QMUI 会帮你调整返回按钮的位置,让箭头与系统默认的返回按钮箭头对齐。 +@interface QDBackBarButton : QMUINavigationButton + +@property(nonatomic, copy) NSString *countString; +@end + +@interface QDBackBarButtonViewController () + +@property(nonatomic, assign) NSInteger badgeOfPreviousViewController; +@property(nonatomic, strong) QDBackBarButton *backBarButton; +@end + +@implementation QDBackBarButtonViewController + +- (instancetype)init { + return [self initWithStyle:UITableViewStyleGrouped]; +} + +- (void)initTableView { + [super initTableView]; + __weak __typeof(self)weakSelf = self; + self.tableView.qmui_staticCellDataSource = [[QMUIStaticTableViewCellDataSource alloc] initWithCellDataSections:@[ + // section 0 + @[ + ({ + QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; + d.style = UITableViewCellStyleSubtitle; + d.text = @"显示自定义的 backBarButtonItem"; + d.detailText = @"与系统一致,设置在前一个界面,生效在下一个界面"; + d.didSelectBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData) { + [weakSelf clearState]; + + // 为了方便演示,Demo 里会在当前界面设置前一个界面的 qmui_backBarButton,实际业务场景并不会这么写 + weakSelf.badgeOfPreviousViewController = 8; + }; + d; + }), + ({ + QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; + d.style = UITableViewCellStyleSubtitle; + d.text = @"动态更新 backBarButtonItem"; + d.detailText = @"子界面的返回按钮会实时更新"; + d.didSelectBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData) { + [weakSelf clearState]; + + // 为了方便演示,Demo 里会在当前界面设置前一个界面的 qmui_backBarButton,实际业务场景并不会这么写 + weakSelf.badgeOfPreviousViewController = 100; + }; + d; + }), + ({ + QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; + d.text = @"恢复为系统 backBarButtonItem"; + d.didSelectBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData) { + [weakSelf clearState]; + + // 把 qmui_backBarButton 置为 nil 即可恢复系统的返回按钮 + weakSelf.qmui_previousViewController.navigationItem.qmui_backBarButton = nil; + }; + d; + }), + ({ + QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; + d.text = @"同时显示 leftBarButtonItems 和返回按钮"; + d.didSelectBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData) { + [weakSelf clearState]; + + weakSelf.navigationItem.leftItemsSupplementBackButton = YES;// 先让返回按钮能与 leftBarButtonItems 共存 + weakSelf.navigationItem.leftBarButtonItems = @[ + [UIBarButtonItem qmui_closeItemWithTarget:nil action:NULL], + ]; + }; + d; + }), + ({ + QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; + d.text = @"只显示 leftBarButtonItems"; + d.didSelectBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData) { + [weakSelf clearState]; + + weakSelf.navigationItem.leftItemsSupplementBackButton = NO;// 与系统用法一致,通过修改 leftItemsSupplementBackButton 为 NO 避免返回按钮与 leftBarButtonItems 同时显示 + weakSelf.navigationItem.leftBarButtonItems = @[ + [UIBarButtonItem qmui_closeItemWithTarget:nil action:NULL], + ]; + }; + d; + }), + ({ + QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; + d.text = @"hidesBackButton"; + d.accessoryType = QMUIStaticTableViewCellAccessoryTypeSwitch; + d.accessorySwitchBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData, UISwitch * _Nonnull switcher) { + weakSelf.navigationItem.hidesBackButton = switcher.on; + }; + d; + }), + ({ + QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; + d.text = @"切换导航栏的显隐"; + d.detailText = @"导航栏不可见时也应能刷新 backBarButtonItem"; + d.didSelectBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData) { + [weakSelf clearState]; + + [weakSelf.navigationController setNavigationBarHidden:!weakSelf.navigationController.navigationBarHidden animated:YES]; + }; + d; + }), + ], + ]]; +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + self.titleView.title = @"QMUIBackBarButton"; + self.titleView.subtitle = @"支持自定义 View 的 backBarButtonItem"; + self.titleView.style = QMUINavigationTitleViewStyleSubTitleVertical; +} + +- (void)setBadgeOfPreviousViewController:(NSInteger)badgeOfPreviousViewController { + _badgeOfPreviousViewController = badgeOfPreviousViewController; + self.qmui_previousViewController.navigationItem.qmui_backBarButton = badgeOfPreviousViewController > 0 ? ({ + QDBackBarButton *backBarButton = QDBackBarButton.new; + backBarButton.countString = badgeOfPreviousViewController > 99 ? @"99+" : [NSString qmui_stringWithNSInteger:badgeOfPreviousViewController]; + [backBarButton sizeToFit]; + backBarButton; + }) : nil; +} + +- (void)clearState { + [self.tableView qmui_clearsSelection]; + self.navigationItem.leftBarButtonItems = nil; + self.navigationItem.leftItemsSupplementBackButton = NO; +} + +@end + +@interface QDBackBarButton () + +@property(nonatomic, strong) UILabel *countLabel; +@property(nonatomic, assign) UIEdgeInsets countLabelPadding; +@property(nonatomic, assign) CGFloat spacingBetweenImageAndTitle; +@end + +@implementation QDBackBarButton + +- (instancetype)init { + if (self = [self initWithType:QMUINavigationButtonTypeBack]) { + + self.countLabel = UILabel.new; + self.countLabel.font = UIFontBoldMake(14); + self.countLabel.textAlignment = NSTextAlignmentCenter; + self.countLabel.clipsToBounds = YES; + [self addSubview:self.countLabel]; + + self.countLabelPadding = UIEdgeInsetsMake(4, 6, 4, 6); + self.spacingBetweenImageAndTitle = 5; + + [self addTarget:self action:@selector(handlePopEvent) forControlEvents:UIControlEventTouchUpInside]; + } + return self; +} + +- (void)tintColorDidChange { + [super tintColorDidChange]; + UIColor *tintColor = self.tintColor; + self.countLabel.textColor = tintColor; + self.countLabel.backgroundColor = [tintColor colorWithAlphaComponent:.25]; +} + +- (void)setCountString:(NSString *)countString { + _countString = countString; + self.countLabel.text = countString; +} + +- (CGSize)sizeThatFits:(CGSize)size { + CGSize countLabelSize = [self.countLabel sizeThatFits:CGSizeMax]; + countLabelSize.width = countLabelSize.width + UIEdgeInsetsGetHorizontalValue(self.countLabelPadding); + countLabelSize.height = countLabelSize.height + UIEdgeInsetsGetVerticalValue(self.countLabelPadding); + countLabelSize.width = MAX(countLabelSize.width, countLabelSize.height); + CGFloat resultWidth = CGRectGetWidth(self.imageView.frame) + self.spacingBetweenImageAndTitle + countLabelSize.width; + CGFloat resultHeight = MAX(CGRectGetHeight(self.imageView.frame), countLabelSize.height); + + return CGSizeMake(resultWidth, resultHeight); +} + +- (void)layoutSubviews { + [super layoutSubviews]; + self.imageView.qmui_left = 0; + self.imageView.qmui_top = CGFloatGetCenter(self.qmui_height, self.imageView.qmui_height); + CGSize countLabelSize = CGSizeMake(CGRectGetWidth(self.bounds) - self.imageView.qmui_right - self.spacingBetweenImageAndTitle, [self.countLabel sizeThatFits:CGSizeMax].height + UIEdgeInsetsGetVerticalValue(self.countLabelPadding)); + self.countLabel.frame = CGRectMake(CGRectGetMaxX(self.imageView.frame) + self.spacingBetweenImageAndTitle, CGFloatGetCenter(self.qmui_height, countLabelSize.height), countLabelSize.width, countLabelSize.height); + self.countLabel.layer.cornerRadius = countLabelSize.height / 2; +} + +- (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; + self.alpha = highlighted ? UIControlHighlightedAlpha : 1; +} + +- (void)handlePopEvent { + UINavigationController *nav = (UINavigationController *)self.qmui_viewController; + if ([self.qmui_viewController isKindOfClass:UINavigationController.class]) { + + // QMUIBackBarButton 的目的是为了尽量模仿系统原生的返回按钮,而系统返回按钮被点击时会询问 QMUI 这个 delegate,所以这里也要调用一下 + // 实际场景:例如 webView 点返回按钮会执行网页的后退操作而不是 nav 的 pop 操作 + BOOL shouldPop = YES; + if ([nav.topViewController respondsToSelector:@selector(shouldPopViewControllerByBackButtonOrPopGesture:)]) { + shouldPop = [nav.topViewController shouldPopViewControllerByBackButtonOrPopGesture:NO]; + } + if (shouldPop) { + [nav popViewControllerAnimated:YES]; + } + } +} + +@end diff --git a/qmuidemo/Modules/Demos/Lab/QDDropdownNotificationViewController.h b/qmuidemo/Modules/Demos/Lab/QDDropdownNotificationViewController.h new file mode 100644 index 00000000..bbf2da56 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QDDropdownNotificationViewController.h @@ -0,0 +1,17 @@ +// +// QDDropdownNotificationViewController.h +// qmuidemo +// +// Created by molice on 2021/10/27. +// Copyright © 2021 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDDropdownNotificationViewController : QDCommonViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Lab/QDDropdownNotificationViewController.m b/qmuidemo/Modules/Demos/Lab/QDDropdownNotificationViewController.m new file mode 100644 index 00000000..c8e78040 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QDDropdownNotificationViewController.m @@ -0,0 +1,222 @@ +// +// QDDropdownNotificationViewController.m +// qmuidemo +// +// Created by molice on 2021/10/27. +// Copyright © 2021 QMUI Team. All rights reserved. +// + +#import "QDDropdownNotificationViewController.h" +#import "QMUIDropdownNotification.h" + +@interface QDDropdownNotificationView : UIControl + +@property(nonatomic, strong) UIImageView *imageView; +@property(nonatomic, strong) UILabel *titleLabel; +@property(nonatomic, strong) UILabel *descriptionLabel; +@property(nonatomic, strong) UILabel *timeLabel; +@property(nonatomic, strong) UIVisualEffectView *backgroundView; + +@property(nonatomic, assign) UIEdgeInsets padding; +@end + +@interface QDDropdownNotificationViewController () + +@property(nonatomic, strong) QMUIButton *button1; +@property(nonatomic, strong) QMUIButton *button2; +@property(nonatomic, strong) QMUIButton *button3; +@property(nonatomic, strong) CALayer *separatorLayer1; +@property(nonatomic, strong) CALayer *separatorLayer2; +@property(nonatomic, strong) CALayer *separatorLayer3; +@end + +@implementation QDDropdownNotificationViewController + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + +} + +- (void)viewDidLoad { + [super viewDidLoad]; + self.button1 = [QDUIHelper generateLightBorderedButton]; + [self.button1 addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.button1 setTitle:@"显示一条自动消失的通知" forState:UIControlStateNormal]; + [self.view addSubview:self.button1]; + + self.button2 = [QDUIHelper generateLightBorderedButton]; + [self.button2 addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.button2 setTitle:@"显示一条不可消除的通知" forState:UIControlStateNormal]; + [self.view addSubview:self.button2]; + + self.button3 = [QDUIHelper generateLightBorderedButton]; + [self.button3 addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.button3 setTitle:@"显示一条自定义 View 的通知" forState:UIControlStateNormal]; + [self.view addSubview:self.button3]; + + self.separatorLayer1 = [CALayer qmui_separatorLayer]; + [self.view.layer addSublayer:self.separatorLayer1]; + + self.separatorLayer2 = [CALayer qmui_separatorLayer]; + [self.view.layer addSublayer:self.separatorLayer2]; + + self.separatorLayer3 = [CALayer qmui_separatorLayer]; + [self.view.layer addSublayer:self.separatorLayer3]; +} + +- (void)handleButtonEvent:(QMUIButton *)button { + if (button == self.button1) { + QMUIDropdownNotification *notification = [QMUIDropdownNotification notificationWithViewClass:QMUIDropdownNotificationView.class configuration:^(QMUIDropdownNotificationView * _Nonnull view) { + view.imageView.image = [UIImage qmui_imageWithColor:UIColor.qd_tintColor size:CGSizeMake(16, 16) cornerRadius:1.5]; + + view.titleLabel.text = @"王者荣耀给你发了一条私信"; + view.titleLabel.textColor = UIColor.qd_titleTextColor; + + view.descriptionLabel.text = @"又输了?点击查看主播的视频教程学学再去玩吧。"; + view.descriptionLabel.textColor = UIColor.qd_mainTextColor; + + view.backgroundView.effect = UIVisualEffect.qd_standardBlurEffect; + view.backgroundView.qmui_foregroundColor = nil; + }]; + notification.didTouchBlock = ^(__kindof QMUIDropdownNotification * _Nonnull notification) { + [notification hide]; + }; + [notification show]; + return; + } + + if (button == self.button2) { + QMUIDropdownNotification *notification = [QMUIDropdownNotification notificationWithViewClass:QMUIDropdownNotificationView.class configuration:^(QMUIDropdownNotificationView * _Nonnull view) { + view.imageView.image = [UIImage qmui_imageWithColor:UIColor.qd_tintColor size:CGSizeMake(16, 16) cornerRadius:1.5]; + + view.titleLabel.text = @"不可消失的通知"; + view.titleLabel.textColor = UIColor.qd_titleTextColor; + + view.descriptionLabel.text = @"用户一定要点击才可以,俗称牛皮藓"; + view.descriptionLabel.textColor = UIColor.qd_mainTextColor; + + view.backgroundView.effect = UIVisualEffect.qd_standardBlurEffect; + view.backgroundView.qmui_foregroundColor = nil; + }]; + notification.canHide = NO;// 不可消失 + notification.didTouchBlock = ^(__kindof QMUIDropdownNotification * _Nonnull notification) { + notification.canHide = YES;// 点击后改为可消失 + [notification hide]; + }; + [notification show]; + return; + } + + if (button == self.button3) { + QMUIDropdownNotification *notification = [QMUIDropdownNotification notificationWithViewClass:QDDropdownNotificationView.class configuration:^(QDDropdownNotificationView * _Nonnull view) { + view.imageView.image = [UIImage qmui_imageWithColor:QDCommonUI.randomThemeColor size:CGSizeMake(100, 100) cornerRadius:8]; + view.titleLabel.text = @"自定义通知的标题"; + view.descriptionLabel.text = @"特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字特别长的文字"; + view.timeLabel.text = @"23:10"; + }]; + notification.canHide = NO;// 不可消失 + notification.didTouchBlock = ^(__kindof QMUIDropdownNotification * _Nonnull notification) { + notification.canHide = YES;// 点击后改为可消失 + [notification hide]; + }; + [notification show]; + return; + } +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + CGFloat contentMinY = self.qmui_navigationBarMaxYInViewCoordinator; + CGFloat buttonSpacingHeight = QDButtonSpacingHeight; + + self.button1.frame = CGRectSetXY(self.button1.frame, CGFloatGetCenter(CGRectGetWidth(self.view.bounds), CGRectGetWidth(self.button1.frame)), contentMinY + CGFloatGetCenter(buttonSpacingHeight, CGRectGetHeight(self.button1.frame))); + self.separatorLayer1.frame = CGRectFlatMake(0, contentMinY + buttonSpacingHeight, CGRectGetWidth(self.view.bounds), PixelOne); + + self.button2.frame = CGRectSetY(self.button1.frame, CGRectGetMaxY(self.separatorLayer1.frame) + CGFloatGetCenter(buttonSpacingHeight, CGRectGetHeight(self.button1.frame))); + self.separatorLayer2.frame = CGRectSetY(self.separatorLayer1.frame, contentMinY + buttonSpacingHeight * 2); + + self.button3.frame = CGRectSetY(self.button1.frame, CGRectGetMaxY(self.separatorLayer2.frame) + CGFloatGetCenter(buttonSpacingHeight, CGRectGetHeight(self.button1.frame))); + self.separatorLayer3.frame = CGRectSetY(self.separatorLayer1.frame, contentMinY + buttonSpacingHeight * 3); +} + + +- (void)setupNavigationItems { + [super setupNavigationItems]; + self.title = @"Dropdown Notification"; +} + +@end + +@implementation QDDropdownNotificationView + +// 自动生成 protocol 里定义的 property(必须) +@synthesize notification; + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + + self.backgroundView = [[UIVisualEffectView alloc] initWithEffect:UIVisualEffect.qd_standardBlurEffect]; + self.backgroundView.userInteractionEnabled = NO; + self.backgroundView.layer.cornerRadius = 12; + self.backgroundView.clipsToBounds = YES; + [self addSubview:self.backgroundView]; + + self.imageView = [[UIImageView alloc] qmui_initWithSize:CGSizeMake(100, 100)]; + self.imageView.contentMode = UIViewContentModeScaleAspectFill; + [self addSubview:self.imageView]; + + self.titleLabel = UILabel.new; + self.titleLabel.font = UIFontMediumMake(15); + self.titleLabel.qmui_lineHeight = round(self.titleLabel.font.pointSize * 1.4); + self.titleLabel.textColor = UIColor.qd_titleTextColor; + [self.titleLabel qmui_calculateHeightAfterSetAppearance]; + [self addSubview:self.titleLabel]; + + self.descriptionLabel = UILabel.new; + self.descriptionLabel.font = UIFontMake(15); + self.descriptionLabel.qmui_lineHeight = round(self.descriptionLabel.font.pointSize * 1.4); + self.descriptionLabel.numberOfLines = 0; + self.descriptionLabel.textColor = UIColor.qd_mainTextColor; + [self addSubview:self.descriptionLabel]; + + self.timeLabel = UILabel.new; + self.timeLabel.font = UIFontMake(12); + self.timeLabel.textColor = UIColor.qd_descriptionTextColor; + [self addSubview:self.timeLabel]; + + self.padding = UIEdgeInsetsMake(18, 16, 18, 16); + } + return self; +} + +- (CGSize)sizeThatFits:(CGSize)size { + CGFloat resultHeight = UIEdgeInsetsGetVerticalValue(self.padding) + CGRectGetHeight(self.imageView.frame); + return CGSizeMake(size.width, resultHeight); +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + self.backgroundView.frame = self.bounds; + + self.imageView.qmui_top = self.padding.top; + self.imageView.qmui_left = self.padding.left; + + [self.timeLabel sizeToFit]; + self.timeLabel.qmui_right = CGRectGetWidth(self.bounds) - self.padding.right; + + CGFloat firstLineHeight = MAX(self.timeLabel.qmui_height, self.titleLabel.qmui_height); + self.titleLabel.qmui_top = self.padding.top + CGFloatGetCenter(firstLineHeight, self.titleLabel.qmui_height); + self.timeLabel.qmui_top = self.padding.top + CGFloatGetCenter(firstLineHeight, self.timeLabel.qmui_height); + + self.titleLabel.qmui_left = self.imageView.qmui_right + 8; + self.titleLabel.qmui_extendToRight = self.timeLabel.qmui_left - 8; + + CGFloat descriptionLabelMarginTop = 8; + CGFloat descriptionLabelMaxHeight = CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.padding) - firstLineHeight - descriptionLabelMarginTop; + CGFloat descriptionLabelWidth = self.timeLabel.qmui_right - self.titleLabel.qmui_left; + CGSize descriptionLabelSize = [self.descriptionLabel sizeThatFits:CGSizeMake(descriptionLabelWidth, CGFLOAT_MAX)]; + self.descriptionLabel.frame = CGRectMake(self.titleLabel.qmui_left, self.padding.top + firstLineHeight + descriptionLabelMarginTop, descriptionLabelWidth, MIN(descriptionLabelSize.height, descriptionLabelMaxHeight)); +} + +@end diff --git a/qmuidemo/Modules/Demos/Lab/QDFontPointSizeAndLineHeightViewController.m b/qmuidemo/Modules/Demos/Lab/QDFontPointSizeAndLineHeightViewController.m index 9e4190c5..acdaa2eb 100644 --- a/qmuidemo/Modules/Demos/Lab/QDFontPointSizeAndLineHeightViewController.m +++ b/qmuidemo/Modules/Demos/Lab/QDFontPointSizeAndLineHeightViewController.m @@ -7,15 +7,23 @@ // #import "QDFontPointSizeAndLineHeightViewController.h" +#import "QMUIInteractiveDebugger.h" @interface QDFontPointSizeAndLineHeightViewController () @property(nonatomic, strong) UILabel *fontPointSizeLabel; @property(nonatomic, strong) UILabel *lineHeightLabel; -@property(nonatomic, strong) QMUISlider *fontPointSizeSlider; +@property(nonatomic, strong) UILabel *glyphLabel; + @property(nonatomic, strong) UILabel *exampleLabel; +@property(nonatomic, strong) UILabel *exampleLabel2; @property(nonatomic, assign) NSInteger oldFontPointSize; +@property(nonatomic, assign) NSInteger newFontPointSize; +@property(nonatomic, assign) CGFloat lineHeightRatio; +@property(nonatomic, assign) BOOL isMedium; + +@property(nonatomic, strong) QMUIInteractiveDebugPanelViewController *asViewController; @end @implementation QDFontPointSizeAndLineHeightViewController @@ -23,13 +31,15 @@ @implementation QDFontPointSizeAndLineHeightViewController - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { self.oldFontPointSize = 16; + self.newFontPointSize = 16; + self.lineHeightRatio = 1.4; } return self; } - (void)initSubviews { [super initSubviews]; - self.fontPointSizeLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(18) textColor:UIColorGray1]; + self.fontPointSizeLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(12) textColor:UIColor.qd_mainTextColor]; [self.fontPointSizeLabel qmui_calculateHeightAfterSetAppearance]; [self.view addSubview:self.fontPointSizeLabel]; @@ -38,58 +48,114 @@ - (void)initSubviews { [self.lineHeightLabel qmui_calculateHeightAfterSetAppearance]; [self.view addSubview:self.lineHeightLabel]; - self.fontPointSizeSlider = [[QMUISlider alloc] init]; - self.fontPointSizeSlider.tintColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; - self.fontPointSizeSlider.thumbSize = CGSizeMake(16, 16); - self.fontPointSizeSlider.thumbColor = self.fontPointSizeSlider.tintColor; - self.fontPointSizeSlider.thumbShadowColor = [self.fontPointSizeSlider.tintColor colorWithAlphaComponent:.3]; - self.fontPointSizeSlider.thumbShadowOffset = CGSizeMake(0, 2); - self.fontPointSizeSlider.thumbShadowRadius = 3; - self.fontPointSizeSlider.minimumValue = 8; - self.fontPointSizeSlider.maximumValue = 50; - self.fontPointSizeSlider.value = self.oldFontPointSize; - [self.fontPointSizeSlider sizeToFit]; - [self.fontPointSizeSlider addTarget:self action:@selector(handleSliderEvent:) forControlEvents:UIControlEventValueChanged]; - [self.view addSubview:self.fontPointSizeSlider]; + self.glyphLabel = [[UILabel alloc] init]; + [self.glyphLabel qmui_setTheSameAppearanceAsLabel:self.fontPointSizeLabel]; + [self.glyphLabel qmui_calculateHeightAfterSetAppearance]; + [self.view addSubview:self.glyphLabel]; self.exampleLabel = [[UILabel alloc] init]; - self.exampleLabel.backgroundColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + self.exampleLabel.backgroundColor = [UIColor.qd_tintColor colorWithAlphaComponent:.3]; self.exampleLabel.textColor = UIColorWhite; - self.exampleLabel.text = @"字体大小与其对应的默认行高"; + self.exampleLabel.text = @"Expel 国";// 中英文不影响,这里为了便于观察,挑选了几个横跨多条线的字母 + self.exampleLabel.qmui_showPrincipalLines = YES; [self.view addSubview:self.exampleLabel]; + self.exampleLabel2 = [[UILabel alloc] init]; + self.exampleLabel2.backgroundColor = [UIColor.qd_tintColor colorWithAlphaComponent:.3]; + self.exampleLabel2.qmui_showPrincipalLines = YES; + [self.view addSubview:self.exampleLabel2]; + + self.asViewController = [self generateDebugController]; + [self.view addSubview:self.asViewController.view]; + [self updateLabelsBaseOnSliderForce:YES]; } -- (void)handleSliderEvent:(UISlider *)slider { - [self updateLabelsBaseOnSliderForce:NO]; -} - - (void)updateLabelsBaseOnSliderForce:(BOOL)force { - NSInteger fontPointSize = (NSInteger)self.fontPointSizeSlider.value; - + NSInteger fontPointSize = self.newFontPointSize; if (force || fontPointSize != self.oldFontPointSize) { - self.exampleLabel.font = UIFontMake(fontPointSize); + UIFont *font = self.isMedium ? UIFontMediumMake(fontPointSize) : UIFontMake(fontPointSize); + self.exampleLabel.font = font; [self.exampleLabel sizeToFit]; - NSInteger lineHeight = (NSInteger)CGRectGetHeight(self.exampleLabel.frame); + CGFloat lineHeight = round(font.pointSize * self.lineHeightRatio); + CGFloat baseline = [QMUIHelper baselineOffsetWhenVerticalAlignCenterInHeight:lineHeight withFont:font]; + + self.exampleLabel2.attributedText = [[NSAttributedString alloc] initWithString:self.exampleLabel.text attributes:@{ + NSFontAttributeName: font, + NSForegroundColorAttributeName: UIColorWhite, + NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:lineHeight], + NSBaselineOffsetAttributeName: @(baseline), + }]; + [self.exampleLabel2 sizeToFit]; - self.fontPointSizeLabel.text = [NSString stringWithFormat:@"字号:%@", @(fontPointSize)]; - self.lineHeightLabel.text = [NSString stringWithFormat:@"行高:%@", @(lineHeight)]; + self.fontPointSizeLabel.text = [NSString stringWithFormat:@"font:%@, descender:%.1f, xHeight:%.1f, capHeight:%.1f", @(fontPointSize), font.descender, font.xHeight, font.capHeight]; + self.lineHeightLabel.text = [NSString stringWithFormat:@"font.lineHeight:%.1f, actually lineHeight:%.1f, baseline:%.1f", font.lineHeight, lineHeight, baseline]; + self.glyphLabel.text = [NSString stringWithFormat:@"label1's location.y:%.1f, label2's location.y:%.1f", [self locationForFirstGlyphInLabel:self.exampleLabel].y, [self locationForFirstGlyphInLabel:self.exampleLabel2].y]; self.oldFontPointSize = fontPointSize; } } +// 得到第一个字符的左上角位置在 label 里的实际渲染坐标(CoreText 坐标系,原点在左下角),理论上应该符合表达式:location.y = capHeight - descender + baselineOffset +- (CGPoint)locationForFirstGlyphInLabel:(UILabel *)label { + NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:label.attributedText]; + NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; + [textStorage addLayoutManager:layoutManager]; + + NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:label.bounds.size]; + textContainer.lineFragmentPadding = 0; + textContainer.maximumNumberOfLines = label.numberOfLines; + [layoutManager addTextContainer:textContainer]; + + NSRange glyphRange; + [layoutManager characterRangeForGlyphRange:NSMakeRange(0, 1) actualGlyphRange:&glyphRange]; + CGPoint glyphLocation = [layoutManager locationForGlyphAtIndex:glyphRange.location]; + return glyphLocation; +} + - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - UIEdgeInsets padding = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.qmui_safeAreaInsets.left, 24 + self.view.qmui_safeAreaInsets.bottom, 24 + self.view.qmui_safeAreaInsets.right); + UIEdgeInsets padding = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.safeAreaInsets.left, 24 + self.view.safeAreaInsets.bottom, 24 + self.view.safeAreaInsets.right); CGFloat contentWidth = CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding); - self.fontPointSizeLabel.frame = CGRectFlatMake(padding.left, padding.top, contentWidth, CGRectGetHeight(self.fontPointSizeLabel.frame)); + CGSize size = [self.asViewController contentSizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)]; + self.asViewController.view.frame = CGRectMake(padding.left, padding.top, contentWidth, size.height); + self.fontPointSizeLabel.frame = CGRectFlatMake(padding.left, CGRectGetMaxY(self.asViewController.view.frame) + 24, contentWidth, CGRectGetHeight(self.fontPointSizeLabel.frame)); self.lineHeightLabel.frame = CGRectFlatMake(padding.left, CGRectGetMaxY(self.fontPointSizeLabel.frame) + 16, contentWidth, CGRectGetHeight(self.lineHeightLabel.frame)); - self.fontPointSizeSlider.frame = CGRectFlatMake(padding.left, CGRectGetMaxY(self.lineHeightLabel.frame) + 16, contentWidth, CGRectGetHeight(self.fontPointSizeSlider.frame)); - - self.exampleLabel.frame = CGRectFlatMake(padding.left, CGRectGetMaxY(self.fontPointSizeSlider.frame) + 40, contentWidth, QMUIViewSelfSizingHeight); + self.glyphLabel.frame = CGRectFlatMake(padding.left, CGRectGetMaxY(self.lineHeightLabel.frame) + 16, contentWidth, CGRectGetHeight(self.glyphLabel.frame)); + self.exampleLabel.frame = CGRectSetXY(self.exampleLabel.frame, padding.left, CGRectGetMaxY(self.glyphLabel.frame) + 24); + self.exampleLabel2.frame = CGRectSetXY(self.exampleLabel2.frame, CGRectGetMaxX(self.exampleLabel.frame) + 8, CGRectGetMaxY(self.exampleLabel.frame) - CGRectGetHeight(self.exampleLabel2.frame)); +} + +- (QMUIInteractiveDebugPanelViewController *)generateDebugController { + __weak __typeof(self)weakSelf = self; + QMUIInteractiveDebugPanelViewController *vc = [QDUIHelper generateDebugViewControllerWithTitle:@"修改字体" items:@[ + [QMUIInteractiveDebugPanelItem sliderItemWithTitle:@"字号" minValue:8 maxValue:50 valueGetter:^(UISlider * _Nonnull actionView) { + actionView.value = round(weakSelf.newFontPointSize); + } valueSetter:^(UISlider * _Nonnull actionView) { + weakSelf.newFontPointSize = round(actionView.value); + [weakSelf updateLabelsBaseOnSliderForce:NO]; + }], + [QMUIInteractiveDebugPanelItem numbericItemWithTitle:@"行高倍数" valueGetter:^(QMUITextField * _Nonnull actionView) { + actionView.text = [NSString stringWithFormat:@"%.2f", weakSelf.lineHeightRatio]; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + weakSelf.lineHeightRatio = actionView.text.doubleValue; + [weakSelf updateLabelsBaseOnSliderForce:YES]; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"加粗" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakSelf.isMedium; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakSelf.isMedium = actionView.on; + [weakSelf updateLabelsBaseOnSliderForce:YES]; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"显示参考线" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakSelf.exampleLabel.qmui_showPrincipalLines; + } valueSetter:^(UISwitch * _Nonnull actionView) { + weakSelf.exampleLabel.qmui_showPrincipalLines = actionView.on; + weakSelf.exampleLabel2.qmui_showPrincipalLines = actionView.on; + }], + ]]; + return vc; } @end diff --git a/qmuidemo/Modules/Demos/Lab/QDInteractiveDebugViewController.h b/qmuidemo/Modules/Demos/Lab/QDInteractiveDebugViewController.h new file mode 100644 index 00000000..a3d2359d --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QDInteractiveDebugViewController.h @@ -0,0 +1,17 @@ +// +// QDInteractiveDebugViewController.h +// qmuidemo +// +// Created by MoLice on 2020/6/18. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDInteractiveDebugViewController : QDCommonViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Lab/QDInteractiveDebugViewController.m b/qmuidemo/Modules/Demos/Lab/QDInteractiveDebugViewController.m new file mode 100644 index 00000000..69a3b077 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QDInteractiveDebugViewController.m @@ -0,0 +1,106 @@ +// +// QDInteractiveDebugViewController.m +// qmuidemo +// +// Created by MoLice on 2020/6/18. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDInteractiveDebugViewController.h" +#import "QMUIInteractiveDebugger.h" + +@interface QDInteractiveDebugViewController () + +@property(nonatomic, strong) QMUIButton *presentButton; +@property(nonatomic, strong) QMUIInteractiveDebugPanelViewController *asViewController; +@end + +@implementation QDInteractiveDebugViewController + +- (void)initSubviews { + [super initSubviews]; + self.presentButton = [QDUIHelper generateLightBorderedButton]; + self.presentButton.contentEdgeInsets = UIEdgeInsetsMake(8, 20, 8, 20); + [self.presentButton setTitle:@"点击打开 Debug 面板" forState:UIControlStateNormal]; + self.presentButton.spacingBetweenImageAndTitle = 4; + [self.presentButton addTarget:self action:@selector(handlePresentButtonEvent) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.presentButton]; + + self.asViewController = [self generateDebugController]; + [self.view addSubview:self.asViewController.view]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + UIEdgeInsets padding = UIEdgeInsetsMake(32 + self.qmui_navigationBarMaxYInViewCoordinator, 32, 32, 32); + [self.presentButton sizeToFit]; + self.presentButton.frame = CGRectSetXY(self.presentButton.frame, CGRectGetMinXHorizontallyCenterInParentRect(self.view.bounds, self.presentButton.frame), padding.top); + CGSize size = [self.asViewController contentSizeThatFits:CGSizeMake(320, CGFLOAT_MAX)]; + self.asViewController.view.frame = CGRectMake(CGFloatGetCenter(CGRectGetWidth(self.view.bounds), 320), CGRectGetMaxY(self.presentButton.frame) + 32, 320, size.height); +} + +- (void)handlePresentButtonEvent { + QMUIInteractiveDebugPanelViewController *vc = [self generateDebugController]; + [vc presentInViewController:self]; +} + +- (QMUIInteractiveDebugPanelViewController *)generateDebugController { + __weak __typeof(self)weakSelf = self; + QMUIInteractiveDebugPanelViewController *vc = [QDUIHelper generateDebugViewControllerWithTitle:@"修改按钮信息" items:@[ + [QMUIInteractiveDebugPanelItem textItemWithTitle:@"文字" valueGetter:^(QMUITextField * _Nonnull actionView) { + actionView.text = weakSelf.presentButton.currentTitle; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + [weakSelf.presentButton setTitle:actionView.text forState:UIControlStateNormal]; + }], + [QMUIInteractiveDebugPanelItem sliderItemWithTitle:@"字号" minValue:8 maxValue:20 valueGetter:^(UISlider * _Nonnull actionView) { + actionView.value = weakSelf.presentButton.titleLabel.font.pointSize; + } valueSetter:^(UISlider * _Nonnull actionView) { + weakSelf.presentButton.titleLabel.font = [weakSelf.presentButton.titleLabel.font fontWithSize:actionView.value]; + [weakSelf.view setNeedsLayout]; + }], + [QMUIInteractiveDebugPanelItem colorItemWithTitle:@"背景色" valueGetter:^(QMUITextField * _Nonnull actionView) { + actionView.text = weakSelf.presentButton.backgroundColor.qmui_RGBAString; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + weakSelf.presentButton.backgroundColor = [UIColor qmui_colorWithRGBAString:actionView.text]; + }], + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"显示图标" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = !!weakSelf.presentButton.currentImage; + } valueSetter:^(UISwitch * _Nonnull actionView) { + [weakSelf.presentButton setImage:actionView.on ? [UIImageMake(@"icon_nav_about") qmui_imageResizedInLimitedSize:CGSizeMake(16, 16)] : nil forState:UIControlStateNormal]; + [weakSelf.view setNeedsLayout]; + }], + ({ + QMUIOrderedDictionary *stateMap = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @(UIControlStateNormal), @"Normal", + @(UIControlStateSelected), @"Selected", + @(UIControlStateDisabled), @"Disabled", nil + ]; + QMUIInteractiveDebugPanelItem *item = [QMUIInteractiveDebugPanelItem enumItemWithTitle:@"切换状态" items:stateMap.allValues valueGetter:^(QMUIButton * _Nonnull actionView, NSArray * _Nonnull items) { + NSString *title = stateMap[@(weakSelf.presentButton.state)] ?: @"Normal"; + [actionView setTitle:title forState:UIControlStateNormal]; + } valueSetter:^(QMUIButton * _Nonnull actionView, NSArray * _Nonnull items) { + NSString *title = actionView.currentTitle; + [stateMap.allValues enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if ([title isEqualToString:obj]) { + UIControlState state = stateMap.allKeys[idx].integerValue; + if (state == UIControlStateSelected) { + weakSelf.presentButton.selected = YES; + weakSelf.presentButton.enabled = YES; + } else if (state == UIControlStateDisabled) { + weakSelf.presentButton.selected = NO; + weakSelf.presentButton.enabled = NO; + } else { + // Normal + weakSelf.presentButton.selected = NO; + weakSelf.presentButton.enabled = YES; + } + } + }]; + }]; + item; + }), + ]]; + return vc; +} + +@end diff --git a/qmuidemo/Modules/Demos/Lab/QDLabViewController.m b/qmuidemo/Modules/Demos/Lab/QDLabViewController.m index 49ada61f..3147f547 100644 --- a/qmuidemo/Modules/Demos/Lab/QDLabViewController.m +++ b/qmuidemo/Modules/Demos/Lab/QDLabViewController.m @@ -7,11 +7,20 @@ // #import "QDLabViewController.h" -#import "QDThemeViewController.h" -#import "QDAnimationViewController.h" +#import "QDCommonListViewController.h" #import "QDAllSystemFontsViewController.h" #import "QDFontPointSizeAndLineHeightViewController.h" #import "QDAboutViewController.h" +#import "QDInteractiveDebugViewController.h" +#import "QDAllAnimationViewController.h" +#import "QDCAShapeLoadingViewController.h" +#import "QDReplicatorLayerViewController.h" +#import "QDRippleAnimationViewController.h" +#import "QDNavigationBarSmoothEffectViewController.h" +#import "QDNavigationBottomAccessoryViewController.h" +#import "QDBackBarButtonViewController.h" +#import "QDDropdownNotificationViewController.h" +#import "QDAnimationCurvesViewController.h" @interface QDLabViewController () @end @@ -21,23 +30,56 @@ @implementation QDLabViewController - (void)initDataSource { [super initDataSource]; self.dataSource = @[@"All System Fonts", - @"Default Line Height", - @"Theme", + @"Font & LineHeight", @"Animation", - @"Log Manager" + @"Log Manager", + @"Interactive Debugger", + @"UINavigationBar Smooth Effect", + @"UINavigationBar Bottom Accessory", + @"Custom BackBarButtonItem", + @"Dropdown Notification", ]; } - (void)didSelectCellWithTitle:(NSString *)title { + __weak __typeof(self)weakSelf = self; UIViewController *viewController = nil; if ([title isEqualToString:@"All System Fonts"]) { viewController = [[QDAllSystemFontsViewController alloc] init]; - } else if ([title isEqualToString:@"Default Line Height"]) { + } else if ([title isEqualToString:@"Font & LineHeight"]) { viewController = [[QDFontPointSizeAndLineHeightViewController alloc] init]; - } else if ([title isEqualToString:@"Theme"]) { - viewController = [[QDThemeViewController alloc] init]; } else if ([title isEqualToString:@"Animation"]) { - viewController = [[QDAnimationViewController alloc] init]; + viewController = ({ + QDCommonListViewController *vc = QDCommonListViewController.new; + vc.dataSource = @[ + @"Animation Curves", + @"Loading", + @"Loading With CAShapeLayer", + @"Animation For CAReplicatorLayer", + @"水波纹" + ]; + vc.didSelectTitleBlock = ^(NSString *title) { + UIViewController *viewController = nil; + if ([title isEqualToString:@"Loading"]) { + viewController = [[QDAllAnimationViewController alloc] init]; + } + else if ([title isEqualToString:@"Loading With CAShapeLayer"]) { + viewController = [[QDCAShapeLoadingViewController alloc] init]; + } + else if ([title isEqualToString:@"Animation For CAReplicatorLayer"]) { + viewController = [[QDReplicatorLayerViewController alloc] init]; + } + else if ([title isEqualToString:@"水波纹"]) { + viewController = [[QDRippleAnimationViewController alloc] init]; + } + else if ([title isEqualToString:@"Animation Curves"]) { + viewController = [[QDAnimationCurvesViewController alloc] init]; + } + viewController.title = title; + [weakSelf.navigationController pushViewController:viewController animated:YES]; + }; + vc; + }); } else if ([title isEqualToString:@"Log Manager"]) { viewController = [[QMUILogManagerViewController alloc] init]; ((QMUILogManagerViewController *)viewController).formatLogNameForSortingBlock = ^NSString *(NSString *logName) { @@ -47,14 +89,28 @@ - (void)didSelectCellWithTitle:(NSString *)title { } return logName; }; + } else if ([title isEqualToString:@"Interactive Debugger"]) { + viewController = [[QDInteractiveDebugViewController alloc] init]; + } else if ([title isEqualToString:@"UINavigationBar Smooth Effect"]) { + viewController = [[QDNavigationBarSmoothEffectViewController alloc] init]; + } else if ([title isEqualToString:@"UINavigationBar Bottom Accessory"]) { + viewController = [[QDNavigationBottomAccessoryViewController alloc] init]; + } else if ([title isEqualToString:@"Custom BackBarButtonItem"]) { + viewController = [[QDBackBarButtonViewController alloc] init]; + } else if ([title isEqualToString:@"Dropdown Notification"]) { + viewController = [[QDDropdownNotificationViewController alloc] init]; } viewController.title = title; [self.navigationController pushViewController:viewController animated:YES]; } +- (void)didInitialize { + [super didInitialize]; + self.title = @"Lab"; +} + - (void)setupNavigationItems { [super setupNavigationItems]; - self.title = @"Lab"; self.navigationItem.rightBarButtonItem = [UIBarButtonItem qmui_itemWithImage:UIImageMake(@"icon_nav_about") target:self action:@selector(handleAboutItemEvent)]; AddAccessibilityLabel(self.navigationItem.rightBarButtonItem, @"打开关于界面"); } diff --git a/qmuidemo/Modules/Demos/Lab/QDNavigationBarSmoothEffectViewController.h b/qmuidemo/Modules/Demos/Lab/QDNavigationBarSmoothEffectViewController.h new file mode 100644 index 00000000..73cf0b64 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QDNavigationBarSmoothEffectViewController.h @@ -0,0 +1,17 @@ +// +// QDNavigationBarSmoothEffectViewController.h +// qmuidemo +// +// Created by MoLice on 2020/7/28. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDNavigationBarSmoothEffectViewController : QDCommonViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Lab/QDNavigationBarSmoothEffectViewController.m b/qmuidemo/Modules/Demos/Lab/QDNavigationBarSmoothEffectViewController.m new file mode 100644 index 00000000..a4d67555 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QDNavigationBarSmoothEffectViewController.m @@ -0,0 +1,86 @@ +// +// QDNavigationBarSmoothEffectViewController.m +// qmuidemo +// +// Created by MoLice on 2020/7/28. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDNavigationBarSmoothEffectViewController.h" +#import "UINavigationBar+QMUISmoothEffect.h" + +@interface QDNavigationBarSmoothEffectViewController () + +@property(nonatomic, strong) UIScrollView *scrollView; +@property(nonatomic, strong) UIView *tagView; +@property(nonatomic, strong) UILabel *tipsLabel; +@end + +@implementation QDNavigationBarSmoothEffectViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + // 1. 指定界面的背景色,让它与 navigationBar.barTintColor 相同,只是后者需要带 alpha 半透明 + self.view.backgroundColor = UIColor.qd_backgroundColor; + + self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; + [self.view addSubview:self.scrollView]; + + self.tagView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; + self.tagView.backgroundColor = UIColor.qd_tintColor; + [self.scrollView addSubview:self.tagView]; + + self.tipsLabel = [[UILabel alloc] init]; + self.tipsLabel.numberOfLines = 0; + NSMutableAttributedString *tips = [[NSMutableAttributedString alloc] initWithString:@"注意观察 navigationBar 与 self.view 接触的边缘,在 navigationBar 背后没内容的情况下,navigationBar 和 self.view 会融合到一起,看不出分界线,只有当内容滚动到 navigationBar 背后时才能看到分界线和磨砂效果" attributes:@{NSFontAttributeName: UIFontMake(14), NSForegroundColorAttributeName: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]}]; + NSDictionary *codeAttributes = CodeAttributes(14); + [tips.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { + [tips addAttributes:codeAttributes range:codeRange]; + }]; + self.tipsLabel.attributedText = tips; + [self.scrollView addSubview:self.tipsLabel]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + // 2. 让 UINavigationBar 固定的磨砂前景色去除,改为自定义的,磨砂效果用 UIBlurEffectStyleLight,从而达到与背景色融合在一起看不到分界线的效果 + self.navigationController.navigationBar.qmui_smoothEffect = YES; +} + +- (UIImage *)qmui_navigationBarBackgroundImage { + return nil;// 3.1 QMUI Demo 默认有背景图,这里为了磨砂,去掉背景图,业务如果本来就没背景图则不需要处理 +} + +- (UIColor *)qmui_navigationBarBarTintColor { + // 3.2 去掉 barTintColor,避免盖多一层磨砂前景色影响磨砂效果 + return nil; +} + +- (UIColor *)qmui_navigationBarTintColor { + return UIColor.qd_titleTextColor; +} + +- (UIColor *)qmui_titleViewTintColor { + return self.qmui_navigationBarTintColor; +} + +- (NSString *)customNavigationBarTransitionKey { + return @"smooth"; +} + +- (UIStatusBarStyle)preferredStatusBarStyle { + return [QDThemeManager.currentTheme.themeName isEqualToString:QDThemeIdentifierDark] ? UIStatusBarStyleLightContent : UIStatusBarStyleDarkContent; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + self.scrollView.frame = self.view.bounds; + self.scrollView.contentSize = self.scrollView.bounds.size; + self.tagView.qmui_left = self.tagView.qmui_leftWhenCenterInSuperview; + self.tagView.qmui_top = -self.qmui_navigationBarMaxYInViewCoordinator; + self.tipsLabel.frame = CGRectMake(24 + self.scrollView.safeAreaInsets.left, self.tagView.qmui_bottom + 24, self.scrollView.qmui_width - UIEdgeInsetsGetHorizontalValue(self.scrollView.safeAreaInsets) - 24 * 2, QMUIViewSelfSizingHeight); +} + +@end diff --git a/qmuidemo/Modules/Demos/Lab/QDNavigationBottomAccessoryViewController.h b/qmuidemo/Modules/Demos/Lab/QDNavigationBottomAccessoryViewController.h new file mode 100644 index 00000000..cf783661 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QDNavigationBottomAccessoryViewController.h @@ -0,0 +1,17 @@ +// +// QDNavigationBottomAccessoryViewController.h +// qmuidemo +// +// Created by MoLice on 2020/7/28. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDNavigationBottomAccessoryViewController : QDCommonViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Lab/QDNavigationBottomAccessoryViewController.m b/qmuidemo/Modules/Demos/Lab/QDNavigationBottomAccessoryViewController.m new file mode 100644 index 00000000..47f4ef45 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QDNavigationBottomAccessoryViewController.m @@ -0,0 +1,84 @@ +// +// QDNavigationBottomAccessoryViewController.m +// qmuidemo +// +// Created by MoLice on 2020/7/28. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDNavigationBottomAccessoryViewController.h" +#import "UINavigationItem+QMUIBottomAccessoryView.h" + +@interface QDBottomAccessoryView : UIView + +@property(nonatomic, strong) UILabel *textLabel; +@property(nonatomic, strong) QMUIButton *linkButton; +@end + +@implementation QDNavigationBottomAccessoryViewController + +- (void)setupNavigationItems { + [super setupNavigationItems]; + QDBottomAccessoryView *accessoryView = [[QDBottomAccessoryView alloc] qmui_initWithSize:CGSizeMake(0, 24)]; + self.navigationItem.qmui_bottomAccessoryView = accessoryView; +} + +- (UIImage *)qmui_navigationBarBackgroundImage { + return nil; +} + +- (UIColor *)qmui_navigationBarTintColor { + return UIColor.qd_titleTextColor; +} + +- (UIColor *)qmui_titleViewTintColor { + return self.qmui_navigationBarTintColor; +} + +- (NSString *)customNavigationBarTransitionKey { + return @"bottom"; +} + +- (UIStatusBarStyle)preferredStatusBarStyle { + return [QDThemeManager.currentTheme.themeName isEqualToString:QDThemeIdentifierDark] ? UIStatusBarStyleLightContent : UIStatusBarStyleDarkContent; +} + +@end + +@implementation QDBottomAccessoryView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.bounds = CGRectMake(0, 4, CGRectGetWidth(frame), CGRectGetHeight(frame)); + + self.textLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(12) textColor:UIColor.qd_titleTextColor]; + self.textLabel.text = @"具体说明请"; + [self.textLabel sizeToFit]; + [self addSubview:self.textLabel]; + + self.linkButton = [[QMUIButton alloc] init]; + self.linkButton.titleLabel.font = UIFontMake(12); + [self.linkButton setTitle:@"查看详情" forState:UIControlStateNormal]; + self.linkButton.qmui_borderPosition = QMUIViewBorderPositionBottom; + self.linkButton.qmui_borderWidth = 1; + self.linkButton.qmui_borderColor = self.linkButton.tintColor; + self.linkButton.qmui_outsideEdge = UIEdgeInsetsMake(-12, -12, -12, -12); + [self.linkButton sizeToFit]; + [self addSubview:self.linkButton]; + } + return self; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + CGFloat contentWidth = CGRectGetWidth(self.textLabel.frame) + CGRectGetWidth(self.linkButton.frame); + CGFloat minX = CGFloatGetCenter(CGRectGetWidth(self.bounds), contentWidth); + + self.textLabel.frame = CGRectSetXY(self.textLabel.frame, minX, CGRectGetMinYVerticallyCenterInParentRect(self.bounds, self.textLabel.frame)); + minX = CGRectGetMaxX(self.textLabel.frame); + + self.linkButton.frame = CGRectSetXY(self.linkButton.frame, minX, CGRectGetMinYVerticallyCenterInParentRect(self.bounds, self.linkButton.frame)); +} + +@end diff --git a/qmuidemo/Modules/Demos/Lab/QDThemeViewController.m b/qmuidemo/Modules/Demos/Lab/QDThemeViewController.m deleted file mode 100644 index c0c2bb7a..00000000 --- a/qmuidemo/Modules/Demos/Lab/QDThemeViewController.m +++ /dev/null @@ -1,140 +0,0 @@ -// -// QDThemeViewController.m -// qmuidemo -// -// Created by QMUI Team on 2017/5/10. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#import "QDThemeViewController.h" -#import "QMUIConfigurationTemplate.h" -#import "QMUIConfigurationTemplateGrapefruit.h" -#import "QMUIConfigurationTemplateGrass.h" -#import "QMUIConfigurationTemplatePinkRose.h" - -@interface QDThemeButton : QMUIButton - -@property(nonatomic, strong) UIColor *themeColor; -@property(nonatomic, copy) NSString *themeName; -@end - -@interface QDThemeViewController () - -@property(nonatomic, copy) NSArray *> *themes; -@property(nonatomic, strong) UIScrollView *scrollView; -@property(nonatomic, strong) NSMutableArray *themeButtons; -@end - -@implementation QDThemeViewController - -- (void)didInitialize { - [super didInitialize]; - - // 因为配置表在 QMUIConfiguration 里也可能会自动 init,所以这里为了避免重复 init,就做了复用处理 - NSMutableArray *> *themes = [[NSMutableArray alloc] init]; - NSArray *allThemeClassName = @[NSStringFromClass([QMUIConfigurationTemplate class]), - NSStringFromClass([QMUIConfigurationTemplateGrapefruit class]), - NSStringFromClass([QMUIConfigurationTemplateGrass class]), - NSStringFromClass([QMUIConfigurationTemplatePinkRose class])]; - [allThemeClassName enumerateObjectsUsingBlock:^(NSString * _Nonnull className, NSUInteger idx, BOOL * _Nonnull stop) { - if ([QDThemeManager sharedInstance].currentTheme && [className isEqualToString:NSStringFromClass([QDThemeManager sharedInstance].currentTheme.class)]) { - [themes addObject:[QDThemeManager sharedInstance].currentTheme]; - } else { - [themes addObject:[[NSClassFromString(className) alloc] init]]; - } - }]; - - self.themes = [themes copy]; - self.themeButtons = [[NSMutableArray alloc] init]; -} - -- (void)initSubviews { - [super initSubviews]; - - self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; - [self.view addSubview:self.scrollView]; - - for (NSInteger i = 0, l = self.themes.count; i < l; i++) { - NSObject *theme = self.themes[i]; - BOOL isCurrentTheme = [theme isKindOfClass:[QDThemeManager sharedInstance].currentTheme.class]; - QDThemeButton *themeButton = [[QDThemeButton alloc] init]; - themeButton.themeColor = theme.themeTintColor; - themeButton.themeName = theme.themeName; - themeButton.selected = isCurrentTheme; - [themeButton addTarget:self action:@selector(handleThemeButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self.scrollView addSubview:themeButton]; - [self.themeButtons addObject:themeButton]; - } -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - - self.scrollView.frame = self.view.bounds; - - UIEdgeInsets padding = UIEdgeInsetsMake(24, 24 + self.view.qmui_safeAreaInsets.left, 24, 24 + self.view.qmui_safeAreaInsets.right); - CGFloat buttonSpacing = 24; - CGSize buttonSize = CGSizeFlatted(CGSizeMake(CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(padding), 110)); - CGFloat buttonMinY = padding.top; - for (NSInteger i = 0, l = self.themeButtons.count; i < l; i++) { - QDThemeButton *themeButton = self.themeButtons[i]; - themeButton.frame = CGRectMake(padding.left, buttonMinY, buttonSize.width, buttonSize.height); - buttonMinY = CGRectGetMaxY(themeButton.frame) + (i == l - 1 ? 0 : buttonSpacing); - } - - self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.scrollView.bounds), buttonMinY + padding.bottom); -} - -- (void)handleThemeButtonEvent:(QDThemeButton *)themeButton { - [self.themeButtons enumerateObjectsUsingBlock:^(QDThemeButton * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - obj.selected = themeButton == obj; - }]; - - NSInteger themeIndex = [self.themeButtons indexOfObject:themeButton]; - [QDThemeManager sharedInstance].currentTheme = self.themes[themeIndex]; - [[NSUserDefaults standardUserDefaults] setObject:NSStringFromClass(self.themes[themeIndex].class) forKey:QDSelectedThemeClassName]; -} - -@end - -@implementation QDThemeButton - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - self.titleLabel.font = UIFontMake(14); - self.titleLabel.textAlignment = NSTextAlignmentCenter; - self.titleLabel.backgroundColor = UIColorWhite; - [self setTitleColor:UIColorGray3 forState:UIControlStateNormal]; - - self.layer.borderWidth = PixelOne; - self.layer.borderColor = UIColorMakeWithRGBA(0, 0, 0, .1).CGColor; - self.layer.cornerRadius = 4; - self.layer.masksToBounds = YES; - } - return self; -} - -- (void)setThemeColor:(UIColor *)themeColor { - _themeColor = themeColor; - self.backgroundColor = themeColor; - [self setTitleColor:themeColor forState:UIControlStateSelected]; -} - -- (void)setThemeName:(NSString *)themeName { - _themeName = themeName; - [self setTitle:themeName forState:UIControlStateNormal]; -} - -- (void)setSelected:(BOOL)selected { - [super setSelected:selected]; - self.titleLabel.font = selected ? UIFontBoldMake(14) : UIFontMake(14); -} - -- (void)layoutSubviews { - [super layoutSubviews]; - CGFloat labelHeight = 36; - self.titleLabel.frame = CGRectMake(0, CGRectGetHeight(self.bounds) - labelHeight, CGRectGetWidth(self.bounds), labelHeight); -} - -@end diff --git a/qmuidemo/Modules/Demos/Lab/QMUIBackBarButton/QMUIBackBarButton.h b/qmuidemo/Modules/Demos/Lab/QMUIBackBarButton/QMUIBackBarButton.h new file mode 100644 index 00000000..950389c8 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QMUIBackBarButton/QMUIBackBarButton.h @@ -0,0 +1,56 @@ +/** + * 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. + */ + +// +// QMUIBackBarButton.h +// qmui +// +// Created by QMUI Team on 20/12/03. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + 系统提供了 UINavigationItem.backBarButtonItem 用于指定当前界面的所有下一级界面的返回按钮文字,但只有文字会生效,customView、target-action 事件都会被忽略,所以提供 QMUIBackBarButton 这个组件支持设置一个自定义 view 作为所有下一级界面的返回按钮(常见的场景例如微信聊天界面返回按钮可以显示一个圆形未读数)。 + + 测试要点: + 1. 检查是否可以通过 qmui_backBarButton = nil 恢复为系统返回按钮。 + 2. 检查子界面设置 leftBarButtonItem(s) = nil 是否能继续显示 qmui_backBarButton。 + 3. 检查是否可以正常与 UINavigationItem.leftItemsSupplementBackButton 搭配使用。 + 4. 存在 qmui_backBarButton 并且子界面 leftItemsSupplementBackButton = NO 时,先设置一个 UIBarButtonItem 再设置为 nil,看是否能正确恢复 qmui_backBarButton 的显示。 + 5. 不应当影响手势返回的使用。 + 6. 假设下一级界面的 UINavigationBar 默认隐藏,当某个时机再次显示时,也应该正常看到 QMUIBackBarButton。 + 7. 在 QMUI 里,不应当影响 pop 拦截功能。 + + 对侵入性的说明: + 1. 由于需要保证手势返回正常运转,所以会 hook 返回手势的 delegate 对象的 _gestureRecognizer:shouldReceiveEvent:、gestureRecognizer:shouldReceiveTouch: 方法,可查看本组件的源码确认对业务项目是否有影响。 + 2. 当界面同时显示 QMUIBackBarButton 和其他 left UIBarButtonItem 时,对业务而言,相当于 leftBarButtonItems.count > 0 && leftItemsSupplementBackButton == YES,但由于 QMUIBackBarButton 本质上是一个自定义的 UIBarButtonItem,所以对系统而言,此时相当于 leftBarButtonItems.count > 1 && leftItemsSupplementBackButton == NO,也即组件需要修改 leftItemsSupplementBackButton 的逻辑,让业务在设置为 YES 时,对系统而言实际上是 NO,所以在使用 QMUIBackBarButton 后,leftItemsSupplementBackButton 的 getter/setter 逻辑会有变化,请知悉。 + */ +@interface UINavigationItem (QMUIBackBarButton) + +/** + 设置一个自定义的 view,令当前界面的所有下一级界面都使用这个 view 作为它们的返回按钮。 + */ +@property(nullable, nonatomic, strong) __kindof UIView *qmui_backBarButton; +@end + + +@protocol QMUIBackBarButtonViewControllerSupport + +@optional +/** + 默认情况下当界面 A 设置了 qmui_backBarButton,A push 到的所有子界面都会显示自定义的返回按钮,但如果子界面实现了 QMUIBackBarButtonViewControllerSupport 并在 shouldShowBackBarButton: 里返回 NO,则可以控制自己的返回按钮不要显示 qmui_backBarButton。 + 默认不实现则视为要显示按钮。 + */ +- (BOOL)shouldShowBackBarButton:(__kindof UIView *)button; + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Lab/QMUIBackBarButton/QMUIBackBarButton.m b/qmuidemo/Modules/Demos/Lab/QMUIBackBarButton/QMUIBackBarButton.m new file mode 100644 index 00000000..4df31a79 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QMUIBackBarButton/QMUIBackBarButton.m @@ -0,0 +1,257 @@ +/** + * 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. + */ + +// +// QMUIBackBarButton.m +// qmui +// +// Created by QMUI Team on 20/12/03. +// + +#import "QMUIBackBarButton.h" + +@interface UIView (QMUIBackBarButton) + +/// 用来标志某个 view 是否被设置为某个 UINavigationItem 的 qmui_backBarButton +@property(nonatomic, assign) BOOL qmuibbb_isViewOfQMUIBackBarButton; +@end + +@interface UINavigationItem () + +@property(nonatomic, strong) UIBarButtonItem *qmuibbb_backBarButtonItem; +@property(nonatomic, assign) BOOL qmuibbb_hidesBackButton;// 这个标志位用来记录外面业务真正设置的值,QMUIBackBarButton 因内部需要而修改 UINavigationItem.hidesBackButton 的不会被记录。 +@end + +@implementation UINavigationItem (QMUIBackBarButton) + +QMUISynthesizeIdStrongProperty(qmuibbb_backBarButtonItem, setQmuibbb_backBarButtonItem) +QMUISynthesizeBOOLProperty(qmuibbb_hidesBackButton, setQmuibbb_hidesBackButton) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // 需要用到系统原本的实现,所以用 Exchange 的方式来 hook selector + ExchangeImplementations(UINavigationItem.class, @selector(setLeftBarButtonItems:animated:), @selector(qmuibbb_setLeftBarButtonItems:animated:)); + ExchangeImplementations(UINavigationItem.class, @selector(setHidesBackButton:animated:), @selector(qmuibbb_setHidesBackButton:animated:)); + +#pragma mark - UINavigationBar setItems:animated: + ExtendImplementationOfVoidMethodWithTwoArguments([UINavigationBar class], @selector(setItems:animated:), NSArray *, BOOL, ^(UINavigationBar *navigationBar, NSArray *items, BOOL animated) { + [UINavigationItem qmuibbb_updateNavigationItems:items]; + }); + +#pragma mark - UINavigationBar didMoveToSuperview + ExtendImplementationOfVoidMethodWithoutArguments([UINavigationBar class], @selector(didMoveToSuperview), ^(UINavigationBar *selfObject) { + [UINavigationItem qmuibbb_updateNavigationItems:selfObject.items]; + }); + +#pragma mark - UINavigationController setNavigationBarHidden:animated: + ExtendImplementationOfVoidMethodWithTwoArguments([UINavigationController class], @selector(setNavigationBarHidden:animated:), BOOL, BOOL, ^(UINavigationController *navigationController, BOOL hidden, BOOL animated) { + [UINavigationItem qmuibbb_updateNavigationItems:navigationController.navigationBar.items]; + }); + +#pragma mark - UINavigationBar pushNavigationItem:animated: + OverrideImplementation([UINavigationBar class], @selector(pushNavigationItem:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UINavigationItem *navigationItem, BOOL animated) { + + [UINavigationItem qmuibbb_updateNavigationItems:[selfObject.items arrayByAddingObject:navigationItem]]; + + // call super + void (*originSelectorIMP)(id, SEL, UINavigationItem *, BOOL); + originSelectorIMP = (void (*)(id, SEL, UINavigationItem *, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, navigationItem, animated); + }; + }); + +#pragma mark - UINavigationItem setLeftBarButtonItem:animated: + OverrideImplementation([UINavigationItem class], @selector(setLeftBarButtonItem:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationItem *selfObject, UIBarButtonItem *item, BOOL animated) { + + // call super + void (*originSelectorIMP)(id, SEL, UIBarButtonItem *, BOOL); + originSelectorIMP = (void (*)(id, SEL, UIBarButtonItem *, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, item, animated); + + [UINavigationItem qmuibbb_updateNavigationItem:selfObject]; + }; + }); + +#pragma mark - UINavigationItem setLeftItemsSupplementBackButton: + OverrideImplementation([UINavigationItem class], @selector(setLeftItemsSupplementBackButton:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationItem *selfObject, BOOL supplementBackButton) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, supplementBackButton); + + [UINavigationItem qmuibbb_updateNavigationItem:selfObject]; + }; + }); + }); +} + +static char kAssociatedObjectKey_backBarButton; +- (void)setQmui_backBarButton:(__kindof UIView *)qmui_backBarButton { + objc_setAssociatedObject(self, &kAssociatedObjectKey_backBarButton, qmui_backBarButton, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + qmui_backBarButton.qmuibbb_isViewOfQMUIBackBarButton = YES; + self.qmuibbb_backBarButtonItem = qmui_backBarButton ? [[UIBarButtonItem alloc] initWithCustomView:qmui_backBarButton] : nil; + + [UINavigationItem qmuibbb_updateNavigationItems:self.qmui_navigationBar.items]; + + // 当返回按钮使用 qmui_backBarButton 实现时,保证手势返回正常运转 + // 如果在 QMUI 体系里,还要检查 pop 拦截功能是否正常 + NSObject *delegate = self.qmui_navigationController.interactivePopGestureRecognizer.delegate; + if (qmui_backBarButton && delegate) { + Class delegateClass = delegate.class; + [QMUIHelper executeBlock:^{ + SEL selector1 = NSSelectorFromString(@"_gestureRecognizer:shouldReceiveEvent:"); + SEL selector2 = @selector(gestureRecognizer:shouldReceiveTouch:); + + BOOL (^isShowingQMUIBackBarButtonBlock)(UIView *) = ^BOOL(UIView *view) { + if ([view.qmui_viewController isKindOfClass:UINavigationController.class]) { + UINavigationController *navController = (UINavigationController *)view.qmui_viewController; + UINavigationItem *topItem = navController.navigationBar.topItem; + if (!navController.navigationBarHidden && topItem.leftBarButtonItem && topItem.leftBarButtonItem == topItem.qmui_previousItem.qmuibbb_backBarButtonItem) { + return YES; + } + } + return NO; + }; + // iOS 13.4 及以后的版本使用这个 + if ([delegate respondsToSelector:selector1]) { + OverrideImplementation(delegateClass, selector1, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^BOOL(NSObject *selfObject, UIGestureRecognizer *firstArgv, UIEvent *secondArgv) { + + if (isShowingQMUIBackBarButtonBlock(firstArgv.view)) { + return YES; + } + + // call super + BOOL (*originSelectorIMP)(id, SEL, UIGestureRecognizer *, UIEvent *); + originSelectorIMP = (BOOL (*)(id, SEL, UIGestureRecognizer *, UIEvent *))originalIMPProvider(); + BOOL result = originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); + + return result; + }; + }); + } + + // iOS 13.4 以前的版本用这个 + if ([delegate respondsToSelector:selector2]) { + OverrideImplementation(delegateClass, selector2, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^BOOL(NSObject *selfObject, UIGestureRecognizer *firstArgv, UITouch *secondArgv) { + + if (isShowingQMUIBackBarButtonBlock(firstArgv.view)) { + return YES; + } + + // call super + BOOL (*originSelectorIMP)(id, SEL, UIGestureRecognizer *, UITouch *); + originSelectorIMP = (BOOL (*)(id, SEL, UIGestureRecognizer *, UITouch *))originalIMPProvider(); + BOOL result = originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); + + return result; + }; + }); + } + } oncePerIdentifier:[NSString stringWithFormat:@"QMUIBackBarButton %@", NSStringFromClass(delegateClass)]]; + } +} + ++ (void)qmuibbb_updateNavigationItems:(NSArray *)items { + for (NSInteger i = 0, l = items.count; i < l; i++) { + [UINavigationItem _qmuibbb_updateNavigationItem:items[i] previousItem:i > 0 ? items[i-1] : nil nextItem:i < l - 1 ? items[i+1] : nil]; + } +} + +// 由于 qmuibbb_updateNavigationItem 的实现原理,实际上更新当前 item 的逻辑是在 prevItem 里(每个 item 都更新自己的 nextItem),所以这里 update 了两次 ++ (void)qmuibbb_updateNavigationItem:(UINavigationItem *)item { + UINavigationItem *prevItem = item.qmui_previousItem; + UINavigationItem *nextItem = item.qmui_nextItem; + if (prevItem) { + [UINavigationItem _qmuibbb_updateNavigationItem:prevItem previousItem:prevItem.qmui_previousItem nextItem:item]; + } + [UINavigationItem _qmuibbb_updateNavigationItem:item previousItem:prevItem nextItem:nextItem]; +} + ++ (void)_qmuibbb_updateNavigationItem:(UINavigationItem *)item previousItem:(UINavigationItem *)prevItem nextItem:(UINavigationItem *)nextItem { + if (prevItem && !prevItem.qmuibbb_backBarButtonItem && item.leftBarButtonItem.customView.qmuibbb_isViewOfQMUIBackBarButton) { + NSMutableArray *leftItems = [item qmuibbb_leftBarButtonItemsWithoutCustom]; + [item qmuibbb_setLeftBarButtonItemsAndUpdateSystemBackButton:leftItems]; + } + if (item.qmuibbb_backBarButtonItem && nextItem) { + UIBarButtonItem *backBarButtonItem = item.qmuibbb_backBarButtonItem; + NSMutableArray *leftItems = [nextItem qmuibbb_leftBarButtonItemsWithoutCustom]; + BOOL shouldShowBackButton = (leftItems.count <= 0 && !nextItem.qmuibbb_hidesBackButton) || (leftItems.count > 0 && nextItem.leftItemsSupplementBackButton && !nextItem.qmuibbb_hidesBackButton); + if (shouldShowBackButton) { + UIViewController *nextViewController = nextItem.qmui_viewController; + if (!nextViewController) { + UINavigationController *nav = item.qmui_navigationController; + if (nav.qmui_isPopping) { + nextViewController = [nav.transitionCoordinator viewControllerForKey:UITransitionContextFromViewControllerKey]; + } else { + nextViewController = nav.topViewController; + } + } + if ([nextViewController respondsToSelector:@selector(shouldShowBackBarButton:)]) { + shouldShowBackButton = [((id)nextViewController) shouldShowBackBarButton:backBarButtonItem.customView]; + } + if (shouldShowBackButton) { + [leftItems insertObject:backBarButtonItem atIndex:0]; + } + } + [nextItem qmuibbb_setLeftBarButtonItemsAndUpdateSystemBackButton:leftItems]; + } +} + +- (__kindof UIView *)qmui_backBarButton { + return (UIView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_backBarButton); +} + +- (void)qmuibbb_setLeftBarButtonItems:(NSArray *)items animated:(BOOL)animated { + [self qmuibbb_setLeftBarButtonItems:items animated:animated]; + [UINavigationItem qmuibbb_updateNavigationItem:self]; + [self qmuibbb_updateHidesBackButton]; +} + +- (void)qmuibbb_setLeftBarButtonItemsAndUpdateSystemBackButton:(NSArray *)items { + [self qmuibbb_setLeftBarButtonItems:items animated:NO]; + [self qmuibbb_updateHidesBackButton]; +} + +// 清理当前的 leftBarButtonItems 里的所有自定义返回按钮,避免前一个界面的自定义返回按钮指针变了却没刷新当前界面的旧返回按钮,所以每次都清理掉再重新 add +- (NSMutableArray *)qmuibbb_leftBarButtonItemsWithoutCustom { + NSMutableArray *leftItems = [[[NSMutableArray alloc] initWithArray:self.leftBarButtonItems] qmui_filterWithBlock:^BOOL(UIBarButtonItem * _Nonnull it) { + return !it.customView.qmuibbb_isViewOfQMUIBackBarButton; + }].mutableCopy; + return leftItems; +} + +- (void)qmuibbb_setHidesBackButton:(BOOL)hidesBackButton animated:(BOOL)animated { + [self qmuibbb_setHidesBackButton:hidesBackButton animated:animated]; + self.qmuibbb_hidesBackButton = hidesBackButton; + [UINavigationItem qmuibbb_updateNavigationItem:self]; +} + +- (void)qmuibbb_updateHidesBackButton { + // 当需要显示自定义的返回按钮时,必须把系统的返回按钮隐藏掉,否则会看到两个返回按钮同时存在 + BOOL shouldShowCustomBackButton = self.leftBarButtonItems.firstObject.customView.qmuibbb_isViewOfQMUIBackBarButton && (self.leftBarButtonItems.count == 1 || (self.leftBarButtonItems.count > 1 && self.leftItemsSupplementBackButton)) && !self.qmuibbb_hidesBackButton; + if (shouldShowCustomBackButton) { + [self qmuibbb_setHidesBackButton:YES animated:NO]; + } else { + [self qmuibbb_setHidesBackButton:self.qmuibbb_hidesBackButton animated:NO]; + } +} + +@end + +@implementation UIView (QMUIBackBarButton) + +QMUISynthesizeBOOLProperty(qmuibbb_isViewOfQMUIBackBarButton, setQmuibbb_isViewOfQMUIBackBarButton) +@end diff --git a/qmuidemo/Modules/Demos/Lab/QMUIDropdownNotification/QMUIDropdownNotification.h b/qmuidemo/Modules/Demos/Lab/QMUIDropdownNotification/QMUIDropdownNotification.h new file mode 100644 index 00000000..4460d3cb --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QMUIDropdownNotification/QMUIDropdownNotification.h @@ -0,0 +1,84 @@ +// +// QMUIDropdownNotification.h +// qmuidemo +// +// Created by molice on 2021/10/27. +// Copyright © 2021 QMUI Team. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIDropdownNotification; + +@protocol QMUIDropdownNotificationViewProtocol + +@required +// 靠 notificationView 去持有 notification,从而避免 notification 被释放 +@property(nullable, nonatomic, strong) __kindof QMUIDropdownNotification *notification; + +@optional +- (void)willShowNotification; +- (void)didShowNotification; +- (void)willHideNotification; +- (void)didHideNotification; + +@end + +/// 作用于 QMUIDropdownNotification.duration 属性,表示该 notification 不会自动消失 +extern const NSTimeInterval QMUIDropdownNotificationDurationInfinite; + +/** + 用来在 App 顶部显示一个 App 内的通知 tips,支持使用自定义的 View 来展示(通常也建议业务自定义自己的 View,QMUI 自带的只能满足最简单的场景)。示例代码: + @code + QMUIDropdownNotification *notification = [QMUIDropdownNotification notificationWithViewClass:QMUIDropdownNotificationView.class configuration:^(QMUIDropdownNotificationView * _Nonnull view) { + view.imageView.image = [UIImage qmui_imageWithColor:UIColor.qd_tintColor size:CGSizeMake(16, 16) cornerRadius:1.5]; + view.titleLabel.text = @"标题"; + view.descriptionLabel.text = @"详细文本"; + }]; + notification.didTouchBlock = ^(__kindof QMUIDropdownNotification * _Nonnull notification) { + [notification hide]; + }; + [notification show]; + @endcode + */ +@interface QMUIDropdownNotification : NSObject + ++ (instancetype)notificationWithViewClass:(Class)viewClass configuration:(void (^ __nullable)(__kindof UIControl *view))configuration; + +/// 获取/设置该 notification 对应的 view。该 view 的点击事件可以业务自己添加,也可以通过 didTouchBlock 来绑定。 +@property(nullable, nonatomic, strong) __kindof UIControl *view; + +/// 表示 notification 显示的持续时长,到达该时长后自动消失。默认为 3s,可以赋值为 QMUIDropdownNotificationDurationInfinite 以使其不会自动消失。 +@property(nonatomic, assign) NSTimeInterval duration; + +/// 表示当前 notification 是否能被消除,默认为 YES。当为 NO 时,不管是调用 hide 方法,或是手动往上滑动,都无法将 notification 消除。 +@property(nonatomic, assign) BOOL canHide; + +/// 通过 block 的形式设置 notification 布局时的上下左右 margin。该 block 会在任何必要的时候被调用(例如状态栏高度变化、横竖屏旋转、iPad 分屏调整等),因此可以在内部动态根据当前实时的 App 状态去布局。 +/// @note 利用该 block 也可以控制 notification 的“最大宽度”——因为 notification 的宽度是由当前 App 宽度减去 block 返回的 left/right 的值得到的。 +@property(nonatomic, copy) UIEdgeInsets (^layoutMarginsBlock)(void); + +/// 是否正在显示 +@property(nonatomic, assign, readonly, getter=isVisible) BOOL visible; + +/// 显示该 notification,此时要求 view 必须非空。 +- (void)show; + +/// 隐藏该 notification。 +- (void)hide; + +@property(nullable, nonatomic, copy) void (^didTouchBlock)(__kindof QMUIDropdownNotification *notification); +@property(nullable, nonatomic, copy) void (^didHideBlock)(__kindof QMUIDropdownNotification *notification); +@end + +@interface QMUIDropdownNotificationView : UIControl + +@property(nonatomic, strong, readonly) UIImageView *imageView; +@property(nonatomic, strong, readonly) UILabel *titleLabel; +@property(nonatomic, strong, readonly) UILabel *descriptionLabel; +@property(nonatomic, strong, readonly) UIVisualEffectView *backgroundView; +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Lab/QMUIDropdownNotification/QMUIDropdownNotification.m b/qmuidemo/Modules/Demos/Lab/QMUIDropdownNotification/QMUIDropdownNotification.m new file mode 100644 index 00000000..8a2096b1 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QMUIDropdownNotification/QMUIDropdownNotification.m @@ -0,0 +1,259 @@ +// +// QMUIDropdownNotification.m +// qmuidemo +// +// Created by molice on 2021/10/27. +// Copyright © 2021 QMUI Team. All rights reserved. +// + +#import "QMUIDropdownNotification.h" + +const NSTimeInterval QMUIDropdownNotificationDurationInfinite = -1; + +@interface QMUIDropdownNotification () + +@property(nonatomic, strong) QMUIModalPresentationViewController *modalPresentationViewController; +@property(nonatomic, strong) UIPanGestureRecognizer *panGesture; +@end + +@implementation QMUIDropdownNotification + ++ (instancetype)notificationWithViewClass:(Class)viewClass configuration:(void (^)(__kindof UIControl * _Nonnull))configuration { + QMUIAssert([viewClass isSubclassOfClass:UIControl.class] && [viewClass conformsToProtocol:@protocol(QMUIDropdownNotificationViewProtocol)], @"QMUIDropdownNotification", @"viewClass 必须是 UIControl 类型的,当前的为 %@", NSStringFromClass(viewClass)); + + QMUIDropdownNotification *notification = [[self alloc] init]; + notification.view = [[viewClass alloc] init]; + if (configuration) { + configuration(notification.view); + } + return notification; +} + +- (instancetype)init { + self = [super init]; + if (self) { + self.duration = 3; + self.canHide = YES; + __weak __typeof(self)weakSelf = self; + self.layoutMarginsBlock = ^UIEdgeInsets{ + CGFloat top = MAX(10, CGRectGetMaxY(UIApplication.sharedApplication.statusBarFrame)); + CGFloat horizontal = 10; + BOOL isPhone = CGRectGetWidth(weakSelf.modalPresentationViewController.window.bounds) / CGRectGetHeight(weakSelf.modalPresentationViewController.window.bounds) < 320.0 / 480.0; + if (!isPhone) { + horizontal = MAX(horizontal, (CGRectGetWidth(weakSelf.modalPresentationViewController.window.bounds) - 400) / 2); + } + return UIEdgeInsetsMake(top, horizontal, 0, horizontal); + }; + self.panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)]; + } + return self; +} + +- (void)show { + QMUIAssert(!!self.view, @"QMUIDropdownNotification", @"%@.view 不存在,无法显示", NSStringFromClass(self.class)); + + if (self.modalPresentationViewController || !self.view) return; + + self.modalPresentationViewController = [[QMUIModalPresentationViewController alloc] init]; + self.modalPresentationViewController.dimmingView = nil; + self.modalPresentationViewController.shouldDimmedAppAutomatically = NO; + self.modalPresentationViewController.shouldBecomeKeyWindow = NO; + self.modalPresentationViewController.contentView = self.view; + if (!self.layoutMarginsBlock) { + // 只是个预防而已 + self.modalPresentationViewController.maximumContentViewWidth = DEVICE_WIDTH - 10 * 2; + } + __weak __typeof(self)weakSelf = self; + self.modalPresentationViewController.layoutBlock = ^(CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewDefaultFrame) { + if (weakSelf.layoutMarginsBlock) { + UIEdgeInsets margins = weakSelf.layoutMarginsBlock(); + CGFloat width = CGRectGetWidth(containerBounds) - UIEdgeInsetsGetHorizontalValue(margins); + weakSelf.view.qmui_frameApplyTransform = CGRectMake(margins.left, margins.top, width, CGRectGetHeight(contentViewDefaultFrame)); + } else { + weakSelf.view.qmui_frameApplyTransform = CGRectSetY(contentViewDefaultFrame, CGRectGetMaxY(UIApplication.sharedApplication.statusBarFrame)); + } + }; + self.modalPresentationViewController.showingAnimation = ^(UIView * _Nullable dimmingView, CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewFrame, void (^ _Nonnull completion)(BOOL)) { + weakSelf.view.transform = CGAffineTransformMakeTranslation(0, -44 - CGRectGetHeight(weakSelf.view.frame)); + [UIView animateWithDuration:.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + weakSelf.view.transform = CGAffineTransformIdentity; + } completion:completion]; + }; + self.modalPresentationViewController.hidingAnimation = ^(UIView * _Nullable dimmingView, CGRect containerBounds, CGFloat keyboardHeight, void (^ _Nonnull completion)(BOOL)) { + // 让 hide 动画能跟随手势结束时的速度 + CGFloat hidingTranslation = -(weakSelf.view.center.y + CGRectGetHeight(weakSelf.view.bounds) / 2); + CGFloat panVelocity = [weakSelf.panGesture velocityInView:weakSelf.view].y; + CGFloat velocity = panVelocity / hidingTranslation; + [UIView animateWithDuration:.4 delay:0 usingSpringWithDamping:1 initialSpringVelocity:velocity options:QMUIViewAnimationOptionsCurveOut animations:^{ + weakSelf.view.transform = CGAffineTransformMakeTranslation(0, hidingTranslation); + } completion:^(BOOL finished) { + weakSelf.view.transform = CGAffineTransformIdentity; + if (completion) completion(finished); + }]; + }; + [self.modalPresentationViewController showWithAnimated:YES completion:^(BOOL finished) { + if ([weakSelf.view respondsToSelector:@selector(didShowNotification)]) { + [weakSelf.view didShowNotification]; + } + + // 自动隐藏 + if (weakSelf.duration > 0) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(weakSelf.duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [weakSelf hide]; + }); + } + }]; + + self.modalPresentationViewController.window.windowLevel = 20000; + self.modalPresentationViewController.window.qmui_hitTestBlock = ^__kindof UIView * _Nullable(CGPoint point, UIEvent * _Nullable event, __kindof UIView * _Nullable originalView) { + if ([originalView isDescendantOfView:weakSelf.view]) return originalView; + return nil; + }; + [self.modalPresentationViewController.window addGestureRecognizer:self.panGesture]; + + if ([self.view respondsToSelector:@selector(willShowNotification)]) { + [self.view willShowNotification]; + } +} + +- (void)hide { + if (!self.canHide || !self.modalPresentationViewController) return; + + if ([self.view respondsToSelector:@selector(willHideNotification)]) { + [self.view willHideNotification]; + } + + __weak __typeof(self)weakSelf = self; + [self.modalPresentationViewController hideWithAnimated:YES completion:^(BOOL finished) { + weakSelf.modalPresentationViewController = nil; + if ([weakSelf.view respondsToSelector:@selector(didHideNotification)]) { + [weakSelf.view didHideNotification]; + } + if (weakSelf.didHideBlock) { + weakSelf.didHideBlock(weakSelf); + } + // 主动断掉持有关系,从而让 notification 得以释放 + weakSelf.view.notification = nil; + }]; +} + +- (BOOL)isVisible { + return self.modalPresentationViewController.visible; +} + +- (void)setView:(__kindof UIControl *)view { + _view = view; + view.notification = self; + [view removeTarget:nil action:@selector(handleNotificationViewTouchEvent:) forControlEvents:UIControlEventTouchUpInside]; + [view addTarget:self action:@selector(handleNotificationViewTouchEvent:) forControlEvents:UIControlEventTouchUpInside]; +} + +- (void)handleNotificationViewTouchEvent:(__kindof UIControl *)view { + if (self.didTouchBlock) { + self.didTouchBlock(self); + } +} + +- (void)handlePanGesture:(UIPanGestureRecognizer *)pan { + if (!self.visible) return; + + CGPoint translation = [pan translationInView:self.view]; + + if (pan.state == UIGestureRecognizerStateChanged) { + if (self.canHide && translation.y < 0) { + self.view.transform = CGAffineTransformMakeTranslation(0, translation.y); + } else { + // 不管是向下,或者是 canHide = NO 时的向上,都不希望拖动,因此用缓动函数让位移越来越迟滞 + CGFloat absTranslation = fabs(translation.y); + CGFloat limitTranslation = translation.y > 0 ? 80 : 20; + CGFloat finalTranslation = [QMUIAnimationHelper bounceFromValue:0 toValue:limitTranslation time:absTranslation / limitTranslation coeff:-1]; + CGFloat progress = finalTranslation / limitTranslation; + finalTranslation *= translation.y < 0 ? -1 : 1; + + CGFloat limitScale = 0.1; + CGFloat finalScale = 1.0 - limitScale * progress; + self.view.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(finalScale, finalScale), CGAffineTransformMakeTranslation(0, finalTranslation)); + } + } else if (pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateCancelled) { + if (!self.canHide || translation.y > 0) { + [UIView animateWithDuration:.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + self.view.transform = CGAffineTransformIdentity; + } completion:nil]; + } else { + if (translation.y <= -15 || (translation.y < 0 && [pan velocityInView:self.view].y < -10)) { + [self hide]; + } + } + } +} + +@end + +@interface QMUIDropdownNotificationView () + +@property(nonatomic, strong, readwrite) UIImageView *imageView; +@property(nonatomic, strong, readwrite) UILabel *titleLabel; +@property(nonatomic, strong, readwrite) UILabel *descriptionLabel; +@property(nonatomic, strong, readwrite) UIVisualEffectView *backgroundView; +@end + +@implementation QMUIDropdownNotificationView + +@synthesize notification; + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + + self.backgroundView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + self.backgroundView.userInteractionEnabled = NO; + self.backgroundView.qmui_foregroundColor = [UIColor.whiteColor colorWithAlphaComponent:.55]; + self.backgroundView.layer.cornerRadius = 12; + self.backgroundView.clipsToBounds = YES; + [self addSubview:self.backgroundView]; + + self.imageView = UIImageView.new; + [self addSubview:self.imageView]; + + self.titleLabel = UILabel.new; + self.titleLabel.font = UIFontMediumMake(15); + self.titleLabel.qmui_lineHeight = round(self.titleLabel.font.pointSize * 1.4); + self.titleLabel.textColor = UIColor.blackColor; + [self addSubview:self.titleLabel]; + + self.descriptionLabel = UILabel.new; + self.descriptionLabel.font = UIFontMake(15); + self.descriptionLabel.qmui_lineHeight = round(self.descriptionLabel.font.pointSize * 1.4); + self.descriptionLabel.textColor = UIColorGray; + [self addSubview:self.descriptionLabel]; + } + return self; +} + +- (CGSize)sizeThatFits:(CGSize)size { + return CGSizeMake(size.width, 82); +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + self.backgroundView.frame = self.bounds; + + UIEdgeInsets padding = UIEdgeInsetsMake(18, 16, 18, 16); + [self.imageView sizeToFit]; + self.imageView.qmui_left = padding.left; + + [self.titleLabel sizeToFit]; + self.titleLabel.qmui_left = self.imageView.qmui_right + 6; + self.titleLabel.qmui_extendToRight = CGRectGetWidth(self.bounds) - padding.right; + + [self.descriptionLabel sizeToFit]; + self.descriptionLabel.qmui_left = padding.left; + self.descriptionLabel.qmui_extendToRight = CGRectGetWidth(self.bounds) - padding.right; + + CGFloat firstLineHeight = MAX(self.imageView.qmui_height, self.titleLabel.qmui_height); + self.imageView.qmui_top = padding.top + CGFloatGetCenter(firstLineHeight, self.imageView.qmui_height); + self.titleLabel.qmui_top = padding.top + CGFloatGetCenter(firstLineHeight, self.titleLabel.qmui_height); + self.descriptionLabel.qmui_top = padding.top + firstLineHeight + 4; +} + +@end diff --git a/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugPanelItem.h b/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugPanelItem.h new file mode 100644 index 00000000..c37e2f2e --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugPanelItem.h @@ -0,0 +1,51 @@ +// +// QMUIInteractiveDebugPanelItem.h +// qmuidemo +// +// Created by QMUI Team on 2020/5/20. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + 代表 Debug 面板里的一行,左边文字右边 actionView,用户操作都是通过 actionView 完成。在 item 被添加到面板上后会调用 valueGetter(),请在里面为自己的 actionView 赋值。当 actionView 产生了用户操作行为(例如输入文字、切换开关)后,numbericItem、colorItem、boolItem 这些内置的 item 会自动调用 valueSetter(),请在里面获取最新的 actionView 的值并进行一些你需要的响应(例如刷新 App 界面上的某些东西)。 + */ +@interface QMUIInteractiveDebugPanelItem : NSObject + +@property(nonatomic, copy) NSString *title; +@property(nonatomic, strong, readonly) UILabel *titleLabel; +@property(nonatomic, strong) __kindof UIView *actionView; +@property(nonatomic, strong, nullable) __kindof NSObject *extraObject; +@property(nonatomic, copy, nullable) void (^valueGetter)(__kindof UIView *actionView); +@property(nonatomic, copy, nullable) void (^valueSetter)(__kindof UIView *actionView); +@property(nonatomic, copy, nullable) void (^valueGetter2)(__kindof UIView *actionView, __kindof NSObject * _Nullable extraObject); +@property(nonatomic, copy, nullable) void (^valueSetter2)(__kindof UIView *actionView, __kindof NSObject * _Nullable extraObject); +@property(nonatomic, assign) CGFloat height; +@property(nonatomic, copy, nullable) void (^didAddBlock)(__kindof QMUIInteractiveDebugPanelItem *item, UIView *containerView); + ++ (instancetype)itemWithTitle:(NSString *)title actionView:(__kindof UIView *)actionView valueGetter:(nullable void (^)(__kindof UIView *actionView))valueGetter valueSetter:(nullable void (^)(__kindof UIView *actionView))valueSetter; + +/// 文字 item,提供一个输入框输入文字 ++ (instancetype)textItemWithTitle:(NSString *)title valueGetter:(nullable void (^)(QMUITextField *actionView))valueGetter valueSetter:(nullable void (^)(QMUITextField *actionView))valueSetter; + +/// 数字 item,提供一个输入框仅允许输入数字、小数点 ++ (instancetype)numbericItemWithTitle:(NSString *)title valueGetter:(nullable void (^)(QMUITextField *actionView))valueGetter valueSetter:(nullable void (^)(QMUITextField *actionView))valueSetter; + +/// 颜色 item,提供一个输入框输入 RGBA 格式的字符串 ++ (instancetype)colorItemWithTitle:(NSString *)title valueGetter:(nullable void (^)(QMUITextField *actionView))valueGetter valueSetter:(nullable void (^)(QMUITextField *actionView))valueSetter; + +/// BOOL 值 item,提供一个 UISwitch 开启/关闭 ++ (instancetype)boolItemWithTitle:(NSString *)title valueGetter:(nullable void (^)(UISwitch *actionView))valueGetter valueSetter:(nullable void (^)(UISwitch *actionView))valueSetter; + +/// enum 值,提供一个按钮点击后会出现菜单,在菜单里选对应的选项 +/// 仅在 iOS 14 及以上版本里可用 ++ (instancetype)enumItemWithTitle:(NSString *)title items:(NSArray *)items valueGetter:(nullable void (^)(QMUIButton *actionView, NSArray *items))valueGetter valueSetter:(nullable void (^)(QMUIButton *actionView, NSArray *items))valueSetter; + +/// 可连续拖动的数值 ++ (instancetype)sliderItemWithTitle:(NSString *)title minValue:(float)minValue maxValue:(float)maxValue valueGetter:(nullable void (^)(UISlider *actionView))valueGetter valueSetter:(nullable void (^)(UISlider *actionView))valueSetter; +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugPanelItem.m b/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugPanelItem.m new file mode 100644 index 00000000..2a323247 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugPanelItem.m @@ -0,0 +1,305 @@ +// +// QMUIInteractiveDebugPanelItem.m +// qmuidemo +// +// Created by QMUI Team on 2020/5/20. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QMUIInteractiveDebugPanelItem.h" + +@interface QMUIInteractiveDebugPanelItem () + +@property(nonatomic, strong, readwrite) UILabel *titleLabel; +@end + +@interface QMUIInteractiveDebugPanelTextItem : QMUIInteractiveDebugPanelItem + +@property(nonatomic, strong) QMUITextField *textField; +@end + +@interface QMUIInteractiveDebugPanelNumbericItem : QMUIInteractiveDebugPanelTextItem +@end + +@interface QMUIInteractiveDebugPanelColorItem : QMUIInteractiveDebugPanelNumbericItem +@end + +@interface QMUIInteractiveDebugPanelBoolItem : QMUIInteractiveDebugPanelItem + +@property(nonatomic, strong) UISwitch *switcher; +@end + +@interface QMUIInteractiveDebugPanelEnumItem : QMUIInteractiveDebugPanelItem + +@property(nonatomic, strong) QMUIButton *menuButton; + +- (instancetype)initWithItems:(NSArray *)items; +@end + +@interface QMUIInteractiveDebugPanelSliderItem : QMUIInteractiveDebugPanelItem + +@property(nonatomic, strong) UISlider *slider; +@end + +@implementation QMUIInteractiveDebugPanelItem + +- (instancetype)init { + self = [super init]; + if (self) { + self.titleLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.blackColor]; + self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; + self.titleLabel.adjustsFontSizeToFitWidth = YES; + self.titleLabel.minimumScaleFactor = .8; + self.titleLabel.numberOfLines = 2; + self.height = 44; + } + return self; +} + +- (void)setTitle:(NSString *)title { + _title = title; + self.titleLabel.text = title; +} + ++ (instancetype)itemWithTitle:(NSString *)title actionView:(__kindof UIView *)actionView valueGetter:(void (^)(__kindof UIView * _Nonnull))valueGetter valueSetter:(void (^)(__kindof UIView * _Nonnull))valueSetter { + QMUIInteractiveDebugPanelItem *item = QMUIInteractiveDebugPanelItem.new; + item.title = title; + item.actionView = actionView; + item.valueGetter = valueGetter; + item.valueSetter = valueSetter; + return item; +} + ++ (instancetype)textItemWithTitle:(NSString *)title valueGetter:(void (^)(QMUITextField * _Nonnull))valueGetter valueSetter:(void (^)(QMUITextField * _Nonnull))valueSetter { + QMUIInteractiveDebugPanelTextItem *item = QMUIInteractiveDebugPanelTextItem.new; + item.title = title; + item.actionView = item.textField; + item.valueGetter = valueGetter; + item.valueSetter = valueSetter; + return item; +} + ++ (instancetype)numbericItemWithTitle:(NSString *)title valueGetter:(void (^)(QMUITextField * _Nonnull))valueGetter valueSetter:(void (^)(QMUITextField * _Nonnull))valueSetter { + QMUIInteractiveDebugPanelNumbericItem *item = QMUIInteractiveDebugPanelNumbericItem.new; + item.title = title; + item.actionView = item.textField; + item.valueGetter = valueGetter; + item.valueSetter = valueSetter; + return item; +} + ++ (instancetype)colorItemWithTitle:(NSString *)title valueGetter:(void (^)(QMUITextField * _Nonnull))valueGetter valueSetter:(void (^)(QMUITextField * _Nonnull))valueSetter { + QMUIInteractiveDebugPanelColorItem *item = QMUIInteractiveDebugPanelColorItem.new; + item.title = title; + item.actionView = item.textField; + item.valueGetter = valueGetter; + item.valueSetter = valueSetter; + return item; +} + ++ (instancetype)boolItemWithTitle:(NSString *)title valueGetter:(void (^)(UISwitch * _Nonnull))valueGetter valueSetter:(void (^)(UISwitch * _Nonnull))valueSetter { + QMUIInteractiveDebugPanelBoolItem *item = QMUIInteractiveDebugPanelBoolItem.new; + item.title = title; + item.actionView = item.switcher; + item.valueGetter = valueGetter; + item.valueSetter = valueSetter; + return item; +} + ++ (instancetype)enumItemWithTitle:(NSString *)title items:(NSArray *)items valueGetter:(void (^)(QMUIButton * _Nonnull, NSArray * _Nonnull))valueGetter valueSetter:(void (^)(QMUIButton * _Nonnull, NSArray * _Nonnull))valueSetter { + QMUIInteractiveDebugPanelEnumItem *item = [[QMUIInteractiveDebugPanelEnumItem alloc] initWithItems:items]; + item.extraObject = items; + item.title = title; + item.actionView = item.menuButton; + item.valueGetter2 = valueGetter; + item.valueSetter2 = valueSetter; + return item; +} + ++ (instancetype)sliderItemWithTitle:(NSString *)title minValue:(float)minValue maxValue:(float)maxValue valueGetter:(void (^)(UISlider * _Nonnull))valueGetter valueSetter:(void (^)(UISlider * _Nonnull))valueSetter { + QMUIInteractiveDebugPanelSliderItem *item = QMUIInteractiveDebugPanelSliderItem.new; + item.title = title; + item.actionView = item.slider; + item.slider.minimumValue = minValue; + item.slider.maximumValue = maxValue; + item.valueGetter = valueGetter; + item.valueSetter = valueSetter; + return item; +} + +@end + +@implementation QMUIInteractiveDebugPanelTextItem + +- (QMUITextField *)textField { + if (!_textField) { + _textField = [[QMUITextField alloc] qmui_initWithSize:CGSizeMake(160, 38)]; + _textField.returnKeyType = UIReturnKeyDone; + _textField.font = [UIFont fontWithName:@"Menlo" size:14]; + _textField.textColor = UIColor.blackColor; + _textField.borderStyle = UITextBorderStyleNone; + _textField.qmui_borderWidth = PixelOne; + _textField.qmui_borderPosition = QMUIViewBorderPositionBottom; + _textField.qmui_borderColor = [UIColorBlack colorWithAlphaComponent:.3]; + _textField.textAlignment = NSTextAlignmentRight; + _textField.delegate = self; + [_textField addTarget:self action:@selector(handleTextFieldChanged:) forControlEvents:UIControlEventEditingChanged]; + } + return _textField; +} + +- (void)handleTextFieldChanged:(QMUITextField *)textField { + if (!textField.isFirstResponder) return; + if (self.valueSetter) self.valueSetter(textField); +} + +#pragma mark - + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + [textField endEditing:YES]; + return YES; +} + +@end + +@implementation QMUIInteractiveDebugPanelNumbericItem + +- (QMUITextField *)textField { + QMUITextField *textField = [super textField]; + textField.keyboardType = UIKeyboardTypeDecimalPad; + return textField; +} + +#pragma mark - + +- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { + // 删除文字 + if (range.length > 0 && string.length <= 0) { + return YES; + } + + return !![string qmui_stringMatchedByPattern:@"[-\\d\\.]"];// 模拟器里,通过电脑键盘输入“点”,输出的可能是中文的句号 +} + +@end + +@implementation QMUIInteractiveDebugPanelColorItem + +- (QMUITextField *)textField { + QMUITextField *textField = [super textField]; + textField.placeholder = @"255,255,255,1.0"; + return textField; +} + +#pragma mark - + +- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { + // 删除文字 + if (range.length > 0 && string.length <= 0) { + return YES; + } + + return !![string qmui_stringMatchedByPattern:@"[\\d\\s\\,\\.]+"]; +} + +@end + +@implementation QMUIInteractiveDebugPanelBoolItem + +- (UISwitch *)switcher { + if (!_switcher) { + _switcher = [[UISwitch alloc] init]; + _switcher.layer.anchorPoint = CGPointMake(.5, .5); + _switcher.transform = CGAffineTransformMakeScale(.7, .7); + [_switcher addTarget:self action:@selector(handleSwitchEvent:) forControlEvents:UIControlEventValueChanged]; + } + return _switcher; +} + +- (void)handleSwitchEvent:(UISwitch *)switcher { + if (self.valueSetter) self.valueSetter(switcher); +} + +@end + +@implementation QMUIInteractiveDebugPanelEnumItem + +- (instancetype)initWithItems:(NSArray *)items { + if (self = [super init]) { + __weak __typeof(self)weakSelf = self; + _menuButton = [[QMUIButton alloc] qmui_initWithSize:CGSizeMake(160, 32)]; + _menuButton.adjustsTitleTintColorAutomatically = YES; + _menuButton.adjustsImageTintColorAutomatically = YES; + _menuButton.layer.borderColor = UIColorSeparator.CGColor; + _menuButton.layer.borderWidth = PixelOne; + _menuButton.layer.cornerRadius = 5; + _menuButton.titleLabel.font = [UIFont fontWithName:@"Menlo" size:14]; + UIImage *triangle = [UIImage qmui_imageWithShape:QMUIImageShapeTriangle size:CGSizeMake(8, 5) tintColor:UIColor.blackColor]; + [_menuButton setImage:[[triangle qmui_imageWithOrientation:UIImageOrientationDown] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateNormal]; + [_menuButton setImage:[triangle imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateHighlighted]; + _menuButton.imagePosition = QMUIButtonImagePositionRight; + _menuButton.spacingBetweenImageAndTitle = 4; + if (@available(iOS 14.0, *)) { + self.didAddBlock = ^(QMUIInteractiveDebugPanelEnumItem * _Nonnull item, UIView * _Nonnull containerView) { + item.menuButton.showsMenuAsPrimaryAction = YES; + if (@available(iOS 15.0, *)) { + item.menuButton.menu = [UIMenu menuWithTitle:item.title image:nil identifier:nil options:UIMenuOptionsSingleSelection children:[items qmui_mapWithBlock:^id _Nonnull(NSString * _Nonnull aItem, NSInteger index) { + UIAction *a = [UIAction actionWithTitle:aItem image:nil identifier:nil handler:^(__kindof UIAction * _Nonnull action) { + [weakSelf.menuButton setTitle:action.title forState:UIControlStateNormal]; + if (weakSelf.valueSetter2) { + weakSelf.valueSetter2(weakSelf.menuButton, weakSelf.extraObject); + } + }]; + if ([item.menuButton.currentTitle isEqualToString:aItem]) { + a.state = UIMenuElementStateOn; + } + return a; + }]]; + } else { + // 低于 iOS 15 处理选择不太方便,干脆不支持算了 + item.menuButton.menu = [UIMenu menuWithChildren:[items qmui_mapWithBlock:^id _Nonnull(NSString * _Nonnull aItem, NSInteger index) { + return [UIAction actionWithTitle:aItem image:nil identifier:nil handler:^(__kindof UIAction * _Nonnull action) { + [weakSelf.menuButton setTitle:action.title forState:UIControlStateNormal]; + if (weakSelf.valueSetter2) { + weakSelf.valueSetter2(weakSelf.menuButton, weakSelf.extraObject); + } + }]; + }]]; + } + }; + } else { + _menuButton.enabled = NO; + [_menuButton setTitle:@"仅支持 iOS 14+" forState:UIControlStateNormal]; + } + } + return self; +} + +@end + +@implementation QMUIInteractiveDebugPanelSliderItem + +- (instancetype)init { + if (self = [super init]) { + _slider = [[UISlider alloc] qmui_initWithSize:CGSizeMake(160, 38)]; + _slider.qmui_trackHeight = 1; + _slider.qmui_thumbSize = CGSizeMake(12, 12); + [_slider addTarget:self action:@selector(handleSliderEvent:) forControlEvents:UIControlEventValueChanged]; + self.didAddBlock = ^(QMUIInteractiveDebugPanelSliderItem * _Nonnull item, UIView * _Nonnull containerView) { + [item updateTitle]; + }; + } + return self; +} + +- (void)handleSliderEvent:(UISlider *)slider { + [self updateTitle]; + if (self.valueSetter) self.valueSetter(slider); +} + +- (void)updateTitle { + self.titleLabel.text = [NSString stringWithFormat:@"%@(%.2f)", self.title, self.slider.value]; + [self.titleLabel sizeToFit]; +} + +@end diff --git a/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugPanelViewController.h b/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugPanelViewController.h new file mode 100644 index 00000000..123fd37e --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugPanelViewController.h @@ -0,0 +1,46 @@ +// +// QMUIInteractiveDebugPanelViewController.h +// qmuidemo +// +// Created by MoLice on 2020/5/20. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIInteractiveDebugPanelItem; + +/** + 调试面板,用于展示若干 @c QMUIInteractiveDebugPanelItem ,用 init 方法初始化后通过 @c title 属性设置标题,通过 @c addDebugItem: 添加调试项目,最后用 @c presentInViewController: 展示出来,或者直接把 .view 当成一个普通的 view 添加到界面上。 + @example + QMUIInteractiveDebugPanelViewController *vc = QMUIInteractiveDebugPanelViewController.new; + vc.title = @"title"; + [vc addDebugItem:xxx]; + [vc presentInViewController:xxx]; + + QMUIInteractiveDebugPanelViewController *vc = QMUIInteractiveDebugPanelViewController.new; + vc.title = @"title"; + [vc addDebugItem:xxx]; + CGSize size = [vc contentSizeThatFits:CGSizeMake(320, CGFLOAT_MAX)]; + vc.view.frame = CGRectMake(24, 24 320, size.height); + [xxx addSubview:vc.view]; + */ +@interface QMUIInteractiveDebugPanelViewController : UIViewController + +@property(nullable, nonatomic, strong, readonly) UILabel *titleLabel; +@property(nullable, nonatomic, strong, readonly) NSArray *debugItems; +@property(nullable, nonatomic, copy) void (^styleConfiguration)(QMUIInteractiveDebugPanelViewController *viewController); + +- (void)addDebugItem:(QMUIInteractiveDebugPanelItem *)item; +- (void)removeDebugItem:(QMUIInteractiveDebugPanelItem *)item; +- (void)insertDebugItem:(QMUIInteractiveDebugPanelItem *)item atIndex:(NSUInteger)index; +- (void)removeDebugItemAtIndex:(NSUInteger)index; +- (__kindof QMUIInteractiveDebugPanelItem *)itemMatched:(BOOL (NS_NOESCAPE^)(__kindof QMUIInteractiveDebugPanelItem* item))block; + +- (void)presentInViewController:(UIViewController *)viewController; +- (CGSize)contentSizeThatFits:(CGSize)size; +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugPanelViewController.m b/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugPanelViewController.m new file mode 100644 index 00000000..7c65de84 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugPanelViewController.m @@ -0,0 +1,153 @@ +// +// QMUIInteractiveDebugPanelViewController.m +// qmuidemo +// +// Created by MoLice on 2020/5/20. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QMUIInteractiveDebugPanelViewController.h" +#import "QMUIInteractiveDebugPanelItem.h" + +@interface QMUIInteractiveDebugPanelViewController () + +@property(nonatomic, strong) NSMutableArray *items; +@property(nonatomic, assign) UIEdgeInsets padding; +@property(nonatomic, assign) CGFloat titleMarginBottom; +@end + +@implementation QMUIInteractiveDebugPanelViewController + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { + self.items = NSMutableArray.new; + } + return self; +} + +- (void)setTitle:(NSString *)title { + [super setTitle:title]; + if (self.isViewLoaded) { + self.titleLabel.text = title; + [self.titleLabel sizeToFit]; + } +} + +// 与 QMUIPopupContainerView 搭配使用时,只要没改过的系统默认 view,它就会被系统修改宽度,不知道为什么,这里强制换成普通 view 即可 +- (void)loadView { + self.view = UIView.new; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + self.padding = UIEdgeInsetsMake(20, 24, 24, 24); + self.titleMarginBottom = 8; + + self.view.backgroundColor = UIColor.whiteColor; + self.view.layer.cornerRadius = 16; + self.view.layer.borderWidth = PixelOne; + self.view.layer.borderColor = [UIColorBlack colorWithAlphaComponent:.3].CGColor; + __weak __typeof(self)weakSelf = self; + self.view.qmui_hitTestBlock = ^__kindof UIView * _Nullable(CGPoint point, UIEvent * _Nullable event, __kindof UIView * _Nullable originalView) { + if (originalView == weakSelf.view) { + // 键盘升起时点击面板的空白区域,会触发两次 hitTest,如果第一次就立马 endEditing,那么第二次进来时由于面板已经用 transform 唯一走了,所以 point 会认为在面板外面,于是 originalView 就是 nil,就会响应到 modalPresentationViewController 的 dimmingView 点击,然后导致面板消失,所以这里在 next runloop 再执行键盘降下的操作 + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf.view endEditing:YES]; + }); + } + return originalView; + }; + + _titleLabel = [[UILabel alloc] qmui_initWithFont:UIFontBoldMake(17) textColor:UIColor.blackColor]; + self.titleLabel.text = self.title; + [self.titleLabel sizeToFit]; + [self.view addSubview:self.titleLabel]; + + for (QMUIInteractiveDebugPanelItem *item in self.items) { + [self addDebugItemViewAfterViewLoaded:item]; + } + + if (self.styleConfiguration) { + self.styleConfiguration(self); + } +} + +- (void)addDebugItem:(QMUIInteractiveDebugPanelItem *)item { + [self.items addObject:item]; + [self addDebugItemViewAfterViewLoaded:item]; +} + +- (void)removeDebugItem:(QMUIInteractiveDebugPanelItem *)item { + [self.items removeObject:item]; + [item.titleLabel removeFromSuperview]; + [item.actionView removeFromSuperview]; + if (self.isViewLoaded) { + [self.view setNeedsLayout]; + } +} + +- (void)insertDebugItem:(QMUIInteractiveDebugPanelItem *)item atIndex:(NSUInteger)index { + [self.items insertObject:item atIndex:index]; + [self addDebugItemViewAfterViewLoaded:item]; +} + +- (void)removeDebugItemAtIndex:(NSUInteger)index { + [self removeDebugItem:self.items[index]]; +} + +- (void)addDebugItemViewAfterViewLoaded:(QMUIInteractiveDebugPanelItem *)item { + if (self.isViewLoaded) { + [self.view addSubview:item.titleLabel]; + [self.view addSubview:item.actionView]; + if (item.valueGetter) item.valueGetter(item.actionView); + if (item.valueGetter2) item.valueGetter2(item.actionView, item.extraObject); + if (item.didAddBlock) item.didAddBlock(item, self.view); + [self.view setNeedsLayout]; + } +} + +- (__kindof QMUIInteractiveDebugPanelItem *)itemMatched:(BOOL (NS_NOESCAPE^)(__kindof QMUIInteractiveDebugPanelItem * _Nonnull))block { + return [self.items qmui_firstMatchWithBlock:block]; +} + +- (NSArray *)debugItems { + return self.items.copy; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + self.titleLabel.qmui_left = self.padding.left; + self.titleLabel.qmui_top = self.padding.top; + + CGFloat lastItemMaxY = self.titleLabel.qmui_bottom + self.titleMarginBottom; + for (QMUIInteractiveDebugPanelItem *item in self.items) { + item.actionView.center = CGPointMake(self.view.qmui_width - self.padding.right - CGRectGetWidth(item.actionView.frame) / 2, lastItemMaxY + item.height / 2); + item.titleLabel.frame = CGRectMake(self.padding.left, lastItemMaxY, CGRectGetMinX(item.actionView.frame) - 8 - self.padding.left, item.height); + lastItemMaxY += item.height; + } +} + +- (void)presentInViewController:(UIViewController *)viewController { + QMUIModalPresentationViewController *modal = [[QMUIModalPresentationViewController alloc] init]; + modal.contentViewController = self; + modal.maximumContentViewWidth = 320; + [viewController presentViewController:modal animated:YES completion:nil]; +} + +- (CGSize)contentSizeThatFits:(CGSize)size { + return [self preferredContentSizeInModalPresentationViewController:self.qmui_modalPresentationViewController keyboardHeight:0 limitSize:size]; +} + +#pragma mark - + +- (CGSize)preferredContentSizeInModalPresentationViewController:(QMUIModalPresentationViewController *)controller keyboardHeight:(CGFloat)keyboardHeight limitSize:(CGSize)limitSize { + return CGSizeMake(limitSize.width, UIEdgeInsetsGetVerticalValue(self.padding) + self.titleLabel.qmui_height + self.titleMarginBottom + ({ + CGFloat itemTotalHeight = 0; + for (QMUIInteractiveDebugPanelItem *item in self.items) { + itemTotalHeight += item.height; + } + itemTotalHeight; + })); +} + +@end diff --git a/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugger.h b/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugger.h new file mode 100644 index 00000000..7b243361 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugger.h @@ -0,0 +1,25 @@ +// +// QMUIInteractiveDebugger.h +// qmuidemo +// +// Created by MoLice on 2020/5/19. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import +#import "QMUIInteractiveDebugPanelViewController.h" +#import "QMUIInteractiveDebugPanelItem.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + 快速地创建一个用 QMUIModalPresentationViewController present 出来的浮层,浮层内提供可交互的调试工具用于修改界面上的效果 + 每个浮层是一个 QMUIInteractiveDebugPanelViewController,里面的每一行是一个 QMUIInteractiveDebugPanelItem,其中 item 又提供了多种常见的类型,例如颜色输入框、数字输入框、balabala + */ +@interface QMUIInteractiveDebugger : NSObject + ++ (void)presentTabBarDebuggerInViewController:(UIViewController *)presentingViewController; ++ (void)presentNavigationBarDebuggerInViewController:(UIViewController *)presentingViewController; +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugger.m b/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugger.m new file mode 100644 index 00000000..0c6e3179 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QMUIInteractiveDebugger/QMUIInteractiveDebugger.m @@ -0,0 +1,65 @@ +// +// QMUIInteractiveDebugger.m +// qmuidemo +// +// Created by MoLice on 2020/5/19. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QMUIInteractiveDebugger.h" + +@implementation QMUIInteractiveDebugger + ++ (void)presentTabBarDebuggerInViewController:(UIViewController *)presentingViewController { + QMUIInteractiveDebugPanelViewController *viewController = [[QMUIInteractiveDebugPanelViewController alloc] init]; + viewController.title = @"UITabBar"; + [viewController addDebugItem:[QMUIInteractiveDebugPanelItem colorItemWithTitle:@"分隔线颜色" valueGetter:^(QMUITextField * _Nonnull actionView) { + UIColor *shadowColor = presentingViewController.tabBarController.tabBar.standardAppearance.shadowColor; + actionView.text = shadowColor.qmui_RGBAString; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + UIColor *shadowColor = [UIColor qmui_colorWithRGBAString:actionView.text]; + presentingViewController.tabBarController.tabBar.standardAppearance.shadowColor = shadowColor; + }]]; + [viewController addDebugItem:[QMUIInteractiveDebugPanelItem colorItemWithTitle:@"背景色" valueGetter:^(QMUITextField * _Nonnull actionView) { + UIColor *barTintColor = presentingViewController.tabBarController.tabBar.standardAppearance.backgroundColor; + actionView.text = barTintColor.qmui_RGBAString; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + UIColor *barTintColor = [UIColor qmui_colorWithRGBAString:actionView.text]; + presentingViewController.tabBarController.tabBar.standardAppearance.backgroundColor = barTintColor; + }]]; + [viewController addDebugItem:[QMUIInteractiveDebugPanelItem numbericItemWithTitle:@"标题垂直间距" valueGetter:^(QMUITextField * _Nonnull actionView) { + CGFloat titleOffset = presentingViewController.tabBarController.tabBar.standardAppearance.stackedLayoutAppearance.normal.titlePositionAdjustment.vertical; + actionView.text = [NSString stringWithFormat:@"%.1f", titleOffset]; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + CGFloat offset = actionView.text.floatValue; + presentingViewController.tabBarController.tabBar.standardAppearance.stackedLayoutAppearance.normal.titlePositionAdjustment = UIOffsetMake(0, offset); + [presentingViewController.tabBarController.tabBar.items enumerateObjectsUsingBlock:^(UITabBarItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [obj.qmui_view setNeedsLayout]; + }]; + }]]; + [viewController presentInViewController:presentingViewController]; +} + ++ (void)presentNavigationBarDebuggerInViewController:(UIViewController *)presentingViewController { + QMUIInteractiveDebugPanelViewController *viewController = [[QMUIInteractiveDebugPanelViewController alloc] init]; + viewController.title = @"UINavigationBar"; + [viewController addDebugItem:[QMUIInteractiveDebugPanelItem colorItemWithTitle:@"分隔线颜色" valueGetter:^(QMUITextField * _Nonnull actionView) { + UIColor *shadowColor = presentingViewController.navigationController.navigationBar.standardAppearance.shadowColor; + actionView.text = shadowColor.qmui_RGBAString; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + UIColor *shadowColor = [UIColor qmui_colorWithRGBAString:actionView.text]; + presentingViewController.navigationController.navigationBar.standardAppearance.shadowColor = shadowColor; + }]]; + [viewController addDebugItem:[QMUIInteractiveDebugPanelItem colorItemWithTitle:@"背景色" valueGetter:^(QMUITextField * _Nonnull actionView) { + UIColor *barTintColor = presentingViewController.navigationController.navigationBar.standardAppearance.backgroundColor; + actionView.text = barTintColor.qmui_RGBAString; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + UIColor *barTintColor = [UIColor qmui_colorWithRGBAString:actionView.text]; + presentingViewController.navigationController.navigationBar.translucent = barTintColor.qmui_alpha < 1; + presentingViewController.navigationController.navigationBar.standardAppearance.backgroundColor = barTintColor; + presentingViewController.navigationController.navigationBar.standardAppearance.backgroundImage = nil; + }]]; + [viewController presentInViewController:presentingViewController]; +} + +@end diff --git a/qmuidemo/Modules/Demos/Lab/QMUINavigationBarSmoothEffect/UINavigationBar+QMUISmoothEffect.h b/qmuidemo/Modules/Demos/Lab/QMUINavigationBarSmoothEffect/UINavigationBar+QMUISmoothEffect.h new file mode 100644 index 00000000..5eb81341 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QMUINavigationBarSmoothEffect/UINavigationBar+QMUISmoothEffect.h @@ -0,0 +1,44 @@ +// +// UINavigationBar+QMUISmoothEffect.h +// QMUI +// +// Created by MoLice on 2020/J/14. +// Copyright © 2020 rdgz. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UINavigationBar (QMUISmoothEffect) + +/** + 让 UINavigationBar 背后没有内容的地方,磨砂能够与当前界面的背景色融合在一起,有内容的地方又可以显示出磨砂效果,使界面层级更浅更自然(可参考 Facebook 的 Messenger 效果)。 + 用法: + @code + - (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + UINavigationBar *bar = self.navigationController.navigationBar; + // 一般置为 YES 即可看到效果了,默认会自动把当前界面背景色叠加上 qmui_smoothEffectAlpha 之后作为磨砂的前景色 + bar.qmui_smoothEffect = YES; + + // 如果希望自定义磨砂前景色,则设置这个属性,记得要加上 alpha,不然透不出背后的磨砂 + bar.qmui_effectForegroundColor = [self.view.backgroundColor colorWithAlphaComponent:.6]; + + // 存在 backgroundImage 时就不会出现磨砂,所以要把它清空 + [bar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault]; + + // 避免使用了 UINavigationBarAppearance 的时候多一层 barTintColor 作为磨砂前景色 + [bar.barTintColor = nil; + } + @endcode + + @warning 当开启这个功能时,请自行保证导航栏的 backgroundImage 为空、barTintColor 为空。 + */ +@property(nonatomic, assign) BOOL qmui_smoothEffect; + +/// 自动把当前界面的背景色叠加上这个 alpha 后作为磨砂的前景色,默认值为 0.7。如果希望用自定义的前景色,请设置 @c qmui_effectForegroundColor +@property(nonatomic, assign) CGFloat qmui_smoothEffectAlpha; +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Lab/QMUINavigationBarSmoothEffect/UINavigationBar+QMUISmoothEffect.m b/qmuidemo/Modules/Demos/Lab/QMUINavigationBarSmoothEffect/UINavigationBar+QMUISmoothEffect.m new file mode 100644 index 00000000..077bcd92 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QMUINavigationBarSmoothEffect/UINavigationBar+QMUISmoothEffect.m @@ -0,0 +1,112 @@ +// +// UINavigationBar+QMUISmoothEffect.m +// QMUI +// +// Created by MoLice on 2020/J/14. +// Copyright © 2020 rdgz. All rights reserved. +// + +#import "UINavigationBar+QMUISmoothEffect.h" + +@implementation UINavigationBar (QMUISmoothEffect) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + OverrideImplementation([UINavigationBar class], @selector(setQmui_effectForegroundColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UIColor *firstArgv) { + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + + if (firstArgv) { + selfObject.qmui_smoothEffectAlpha = -1; + } + }; + }); + + // 同步假 bar 的效果 + SEL setterSelector = NSSelectorFromString(@"setQmuinb_copyStylesToBar:"); + NSAssert([UINavigationBar instancesRespondToSelector:setterSelector], @"请检查 UINavigationBar+Transtion 里是否没提供 setQmuinb_copyStylesToBar: 方法?"); + OverrideImplementation(UINavigationBar.class, setterSelector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UINavigationBar *copyStylesToBar) { + + // call super + void (*originSelectorIMP)(id, SEL, UINavigationBar *); + originSelectorIMP = (void (*)(id, SEL, UINavigationBar *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, copyStylesToBar); + + copyStylesToBar.qmui_smoothEffect = selfObject.qmui_smoothEffect; + copyStylesToBar.qmui_smoothEffectAlpha = selfObject.qmui_smoothEffectAlpha; + }; + }); + }); +} + +static char kAssociatedObjectKey_smoothEffect; +- (void)setQmui_smoothEffect:(BOOL)qmui_smoothEffect { + BOOL valueChanged = qmui_smoothEffect != self.qmui_smoothEffect; + if (!valueChanged) return; + if (qmui_smoothEffect) { + + UIImage *backgroundImage = [self backgroundImageForBarMetrics:UIBarMetricsDefault]; + if (backgroundImage && !CGRectIsEmpty(self.frame)) {// 假 bar 转场时有一瞬间会走到这里,但那个时候 frame 还是0,所以屏蔽这个情况 + QMUILogWarn(@"QMUISmoothEffect", @"试图开启 qmui_smoothEffect 但由于当前 UINavigationBar 存在 backgroundImage 所以无法显示磨砂,%@, backgroundImage = %@", self, backgroundImage); + return; + } + + UIColor *barTintColor = self.barTintColor; + if (barTintColor && barTintColor.qmui_alpha > 0 && !CGRectIsEmpty(self.frame)) {// 假 bar 转场时有一瞬间会走到这里,但那个时候 frame 还是0,所以屏蔽这个情况 + // 只是提示,不用 return,因为影响比较小 + QMUILogWarn(@"QMUISmoothEffect", @"开启 qmui_smoothEffect 的 UINavigationBar 同时存在 barTintColor,可能会导致在 iOS 15 上的磨砂效果比 iOS 14 及以前的版本要弱,因为前景色多了一层 barTintColor。bar = %@, barTintColor = %@", self, barTintColor); + } + } + + objc_setAssociatedObject(self, &kAssociatedObjectKey_smoothEffect, @(qmui_smoothEffect), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + if (qmui_smoothEffect) { + self.qmui_effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; + [self qmuinbe_setEffectForegroundColorAutomatically]; + } else { + self.qmui_effect = nil; + } +} + +- (BOOL)qmui_smoothEffect { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_smoothEffect)) boolValue]; +} + +static char kAssociatedObjectKey_smoothEffectAlpha; +- (void)setQmui_smoothEffectAlpha:(CGFloat)smoothEffectAlpha { + objc_setAssociatedObject(self, &kAssociatedObjectKey_smoothEffectAlpha, @(smoothEffectAlpha), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self qmuinbe_setEffectForegroundColorAutomatically]; +} + +- (CGFloat)qmui_smoothEffectAlpha { + NSNumber *value = ((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_smoothEffectAlpha)); + if (!value) { + // 默认值 + return 0.7; + } + return [value qmui_CGFloatValue]; +} + +- (void)qmuinbe_setEffectForegroundColorAutomatically { + // 自动设置完一次就不会再更新了,因为在上面 hook 逻辑里会把 alpha 置为-1,就不会再走进来这个条件 + if (self.qmui_smoothEffectAlpha >= 0) { + UINavigationController *nav = (UINavigationController *)self.qmui_viewController; + if ([nav isKindOfClass:UINavigationController.class]) { + UIViewController *vc = nav.topViewController; + if (vc.isViewLoaded) { + UIColor *color = vc.view.backgroundColor; + color = [color colorWithAlphaComponent:self.qmui_smoothEffectAlpha]; + self.qmui_effectForegroundColor = color; + } + } + } +} + +@end diff --git a/qmuidemo/Modules/Demos/Lab/QMUINavigationBottomAccessoryView/UINavigationItem+QMUIBottomAccessoryView.h b/qmuidemo/Modules/Demos/Lab/QMUINavigationBottomAccessoryView/UINavigationItem+QMUIBottomAccessoryView.h new file mode 100644 index 00000000..18156d15 --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QMUINavigationBottomAccessoryView/UINavigationItem+QMUIBottomAccessoryView.h @@ -0,0 +1,33 @@ +// +// UINavigationItem+QMUIBottomAccessoryView.h +// qmuidemo +// +// Created by MoLice on 2020/7/20. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + 支持在 navigationBar 底部显示一个额外的 view,仅支持 navigationBar 为磨砂的场景。 + 使用方法: + 1. 为 accessoryView 设置好高度,至于 x/y/width 均会被忽略,设置为0即可,最终 accessoryView 宽度会撑满 navigationBar,并被置于 navigationBar 的底部。 + 2. 通过 viewController.navigationItem.qmui_bottomAccessoryView = xxx 的方式设置。 + + @note 以后修改代码时的测试要点 + 因为 navigationItem 是隶属于每个 UIViewController 的,所以会涉及到几个场景: + 1. 将被 push 的 vc 在 viewDidLoad 里修改 navigationItem(意味着 push 时直接拿该 item 即可)。 + 2. 将被 push 的 vc 在 viewWillAppear: 里修改 navigationItem(意味着 push 时拿该 item,然后在 push 过程中又要再次刷新该 item)。 + 3. 在 viewDidAppear: 后修改 navigationItem(此时 push/pop 均已结束,所以需要在 navigationItem 被修改后主动通知界面更新)。 + 4. pop 时在前一个界面的 viewWillAppear: 里修改 navigationItem(因为是先触发 pop 再触发 viewWillAppear:,所以 viewWillAppear: 里修改 navigationItem 可能会覆盖 pop 过程的动画)。 + 5. 手势返回过程、中断又取消手势返回。 + */ +@interface UINavigationItem (QMUIBottomAccessoryView) + +/// 在 navigationBar 底部显示一个额外的 view,仅支持 navigationBar 为磨砂的场景。为这个属性赋值前请自行保证 view 的 height 已被正确设置,至于 x/y/width 均会被忽略,设置为0即可。 +@property(nonatomic, strong) __kindof UIView *qmui_bottomAccessoryView; +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Lab/QMUINavigationBottomAccessoryView/UINavigationItem+QMUIBottomAccessoryView.m b/qmuidemo/Modules/Demos/Lab/QMUINavigationBottomAccessoryView/UINavigationItem+QMUIBottomAccessoryView.m new file mode 100644 index 00000000..7462b0be --- /dev/null +++ b/qmuidemo/Modules/Demos/Lab/QMUINavigationBottomAccessoryView/UINavigationItem+QMUIBottomAccessoryView.m @@ -0,0 +1,248 @@ +// +// UINavigationItem+QMUIBottomAccessoryView.m +// qmuidemo +// +// Created by MoLice on 2020/7/20. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "UINavigationItem+QMUIBottomAccessoryView.h" + +@interface UINavigationBar (QMUIBottomAccessoryView) + +// 对于开启了转场导航栏效果优化(例如配置表 AutomaticCustomNavigationBarTransitionStyle 为 YES)的情况,转场过程中会添加一条复制了目标 bar 所有样式的假 UINavigationBar,如果目标 bar 带有 qmuibav_bottomAccessoryView 则 backgroundView 里的 effectView 高度需要变高,但 qmuibav_bottomAccessoryView 是 add 在目标 bar 上而不是同时也 add 到假 bar 上,为了让假 bar 也能拿到 effectView 需要变高的值,这里通过 block 返回一个值(而不是通过 bar.qmuibav_bottomAccessoryView 取高度) +@property(nonatomic, copy) CGFloat (^qmuibav_backgroundEffectViewExtendBottomBlock)(void); +@property(nonatomic, strong) __kindof UIView *qmuibav_bottomAccessoryView; + +- (void)setQmuibav_bottomAccessoryView:(UIView *)qmuibav_bottomAccessoryView animated:(BOOL)animated;// 前置声明,让 UINavigationItem 里能访问到 +@end + +@implementation UINavigationItem (QMUIBottomAccessoryView) + ++ (void)qmuibav_swizzleMethods { + + #pragma mark - qmui_navigationBarMaxYInViewCoordinator + OverrideImplementation([UIViewController class], @selector(qmui_navigationBarMaxYInViewCoordinator), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGFloat(UIViewController *selfObject) { + // call super + CGFloat (*originSelectorIMP)(id, SEL); + originSelectorIMP = (CGFloat (*)(id, SEL))originalIMPProvider(); + CGFloat result = originSelectorIMP(selfObject, originCMD); + + if (result > 0 && selfObject.navigationItem.qmui_bottomAccessoryView) { + return result + CGRectGetHeight(selfObject.navigationItem.qmui_bottomAccessoryView.frame); + } + return result; + }; + }); + + #pragma mark - TransitionNavigationBar setOriginalNavigationBar: + + // 在开启假 bar 转场效果优化时把 bottomAccessoryView 也同步复制到假 bar + SEL setterSelector = NSSelectorFromString(@"setQmuinb_copyStylesToBar:"); + NSAssert([UINavigationBar instancesRespondToSelector:setterSelector], @"请检查 UINavigationBar+Transtion 里是否没提供 setQmuinb_copyStylesToBar: 方法?"); + OverrideImplementation(UINavigationBar.class, setterSelector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UINavigationBar *copyStylesToBar) { + + // call super + void (*originSelectorIMP)(id, SEL, UINavigationBar *); + originSelectorIMP = (void (*)(id, SEL, UINavigationBar *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, copyStylesToBar); + + copyStylesToBar.qmuibav_backgroundEffectViewExtendBottomBlock = selfObject.qmuibav_backgroundEffectViewExtendBottomBlock; + copyStylesToBar.qmui_backgroundView.qmui_layoutSubviewsBlock = selfObject.qmui_backgroundView.qmui_layoutSubviewsBlock; + }; + }); + + #pragma mark - updateBottomAccessoryViewBlock + void (^updateBottomAccessoryViewBlock)(UINavigationBar *, UINavigationItem *, BOOL) = ^void(UINavigationBar *navigationBar, UINavigationItem *navigationItem, BOOL animated) { + if (navigationBar.qmuibav_bottomAccessoryView != navigationItem.qmui_bottomAccessoryView) { + [navigationBar setQmuibav_bottomAccessoryView:navigationItem.qmui_bottomAccessoryView animated:animated]; + } + }; + + #pragma mark - UINavigationBar pushNavigationItem:animated: + // push 界面时更新 navigationItem + void (^pushNavigationItemBlock)(UINavigationBar *, UINavigationItem *, BOOL) = ^(UINavigationBar *navigationBar, UINavigationItem *navigationItem, BOOL animated) { + updateBottomAccessoryViewBlock(navigationBar, navigationItem, animated); + }; + + #pragma mark - UINavigationBar _popNavigationItemWithTransition: + // pop 界面时更新 navigationItem,系统没有调用 public API `popNavigationItemAnimated:`,所以只能用私有 API + UINavigationItem *(^popNavigationItemWithTransitionBlock)(UINavigationBar *, NSInteger, id) = ^UINavigationItem *(UINavigationBar *navigationBar, NSInteger transition, UINavigationItem *originReturnValue) { + updateBottomAccessoryViewBlock(navigationBar, navigationBar.topItem, transition > 0); + return originReturnValue; + }; + + #pragma mark - UINavigationBar setItems:animated: + // 通过 setItems 直接修改 navigationItem 堆栈时更新 navigationItem + void (^setItemsBlock)(UINavigationBar *, NSArray *, BOOL) = ^(UINavigationBar *navigationBar, NSArray *items, BOOL animated) { + updateBottomAccessoryViewBlock(navigationBar, items.lastObject, animated); + }; + + #pragma mark - UINavigationBar _updateContentIfTopItem:animated: + // 在 viewWillAppear: 等已经显示完 navigationBar 之后的时机再去修改 navigationItem 时,系统会通过这个私有 API 来刷新当前的 navigationItem + void (^updateTopItemBlock)(UINavigationBar *, UINavigationItem *, BOOL) = ^(UINavigationBar *navigationBar, UINavigationItem *navigationItem, BOOL animated) { + if (navigationBar.topItem == navigationItem) {// 在 pop 的时候如果前一个界面在 viewWillAppear: 里修改 navigationItem,则会先触发这个 block,再触发 pop block,导致没有动画,所以做一个保护 + updateBottomAccessoryViewBlock(navigationBar, navigationItem, animated); + } + }; + + #pragma mark - UINavigationBar didMoveToSuperview + // 例如 A 不显示 bottomAccessoryView,在 A 里 present UISearchController,此时 navigationBar 会从 View 层级树里被移除,在 searchController 里进入界面 B,B 显示 bottomAccessoryView,此时从 B 回到 searchController,再降下 searchController,navigationBar 会被重新加回 View 层级树,这时候需要主动刷新一下 navigationItem,否则 A 也会看到 bottomAccessoryView。 + void (^didMoveToSuperviewBlock)(UINavigationBar *) = ^void(UINavigationBar *navigationBar) { + updateBottomAccessoryViewBlock(navigationBar, navigationBar.topItem, NO); + }; + + #pragma mark - UINavigationController setNavigationBarHidden:animated: + // 如果在 navigationBar 隐藏的情况下去修改 navigationItem,是无法触发 updateTopItemBlock 的,所以需要在显示 navigationBar 时主动刷新一次 + void (^navigationBarHiddenBlock)(UINavigationController *, BOOL, BOOL) = ^void(UINavigationController *navigationController, BOOL hidden, BOOL animated) { + if (!hidden) { + updateBottomAccessoryViewBlock(navigationController.navigationBar, navigationController.navigationBar.topItem, animated); + } + }; + + ExtendImplementationOfVoidMethodWithTwoArguments([UINavigationBar class], @selector(pushNavigationItem:animated:), UINavigationItem *, BOOL, pushNavigationItemBlock); + ExtendImplementationOfNonVoidMethodWithSingleArgument([UINavigationBar class], NSSelectorFromString(@"_popNavigationItemWithTransition:"), NSInteger, id, popNavigationItemWithTransitionBlock); + ExtendImplementationOfVoidMethodWithTwoArguments([UINavigationBar class], @selector(setItems:animated:), NSArray *, BOOL, setItemsBlock); + ExtendImplementationOfVoidMethodWithTwoArguments([UINavigationBar class], NSSelectorFromString(@"_updateContentIfTopItem:animated:"), UINavigationItem *, BOOL, updateTopItemBlock); + ExtendImplementationOfVoidMethodWithoutArguments([UINavigationBar class], @selector(didMoveToSuperview), didMoveToSuperviewBlock); + ExtendImplementationOfVoidMethodWithTwoArguments([UINavigationController class], @selector(setNavigationBarHidden:animated:), BOOL, BOOL, navigationBarHiddenBlock); +} + +static char kAssociatedObjectKey_bottomAccessoryView; +- (void)setQmui_bottomAccessoryView:(__kindof UIView *)qmui_bottomAccessoryView { + if (qmui_bottomAccessoryView) { + [QMUIHelper executeBlock:^{ + [self.class qmuibav_swizzleMethods]; + } oncePerIdentifier:[NSString stringWithFormat:@"QMUIBottomAccessoryView %@", NSStringFromSelector(@selector(qmuibav_swizzleMethods))]]; + } + + objc_setAssociatedObject(self, &kAssociatedObjectKey_bottomAccessoryView, qmui_bottomAccessoryView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + // 系统在修改了 UINavigationItem.rightBarButtonItem/titleView 等属性后会通过这个私有方法去通知 navigationBar 刷新内部的按钮,这里也借助这个方法在 navigationBar 内部刷新 bottomAccessoryView + BOOL animated = YES; + [self qmui_performSelector:NSSelectorFromString(@"updateNavigationBarButtonsAnimated:") withArguments:&animated, nil]; +} + +- (__kindof UIView *)qmui_bottomAccessoryView { + return (UIView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_bottomAccessoryView); +} + +@end + +@implementation UINavigationBar (QMUIBottomAccessoryView) + +static char kAssociatedObjectKey_accessoryView; +- (void)setQmuibav_bottomAccessoryView:(UIView *)qmuibav_bottomAccessoryView animated:(BOOL)animated { + BOOL prefersToShow = !self.qmuibav_bottomAccessoryView && qmuibav_bottomAccessoryView; + BOOL prefersToHide = self.qmuibav_bottomAccessoryView && !qmuibav_bottomAccessoryView; + BOOL shouldAnimate = animated && (prefersToShow || prefersToHide);// 只有从无到有或从有到无才需要动画,本来就有的,不管前后是否一致,都不需要动画呈现 + + self.qmuibav_backgroundEffectViewExtendBottomBlock = qmuibav_bottomAccessoryView ? ^CGFloat{ + return CGRectGetHeight(qmuibav_bottomAccessoryView.frame); + } : nil; + + if (!self.qmui_backgroundView.qmui_layoutSubviewsBlock) { + self.qmui_backgroundView.qmui_layoutSubviewsBlock = ^(__kindof UIView * _Nonnull selfObject) { + UINavigationBar *navigationBar = (UINavigationBar *)selfObject.superview; + if (!navigationBar || ![navigationBar isKindOfClass:UINavigationBar.class]) { + return; + } + + UIVisualEffectView *effectView = [selfObject qmui_valueForKey:@"_effectView1"]; + if (navigationBar.qmuibav_backgroundEffectViewExtendBottomBlock) { + if (effectView) { + effectView.frame = CGRectSetHeight(effectView.frame, CGRectGetHeight(selfObject.bounds) + navigationBar.qmuibav_backgroundEffectViewExtendBottomBlock()); + } + } + }; + } + if (!self.qmui_layoutSubviewsBlock) { + self.qmui_layoutSubviewsBlock = ^(UINavigationBar * _Nonnull navigationBar) { + UIView *accessoryView = navigationBar.qmuibav_bottomAccessoryView; + if (accessoryView) { + accessoryView.qmui_frameApplyTransform = CGRectMake(0, CGRectGetHeight(accessoryView.superview.bounds), CGRectGetWidth(accessoryView.superview.bounds), CGRectGetHeight(accessoryView.frame)); + [accessoryView.superview bringSubviewToFront:accessoryView]; + CGRect bounds = navigationBar.bounds; + CGRect boundsWithAccessoryView = CGRectUnion(bounds, accessoryView.frame); + UIEdgeInsets outsideEdge = UIEdgeInsetsMake(MIN(CGRectGetMinY(bounds) - CGRectGetMinY(boundsWithAccessoryView), 0), + MIN(CGRectGetMinX(bounds) - CGRectGetMinX(boundsWithAccessoryView), 0), + MIN(CGRectGetMaxY(bounds) - CGRectGetMaxY(boundsWithAccessoryView), 0), + MIN(CGRectGetMaxX(bounds) - CGRectGetMaxX(boundsWithAccessoryView), 0)); + navigationBar.qmui_outsideEdge = outsideEdge; + } else { + navigationBar.qmui_outsideEdge = UIEdgeInsetsZero; + } + }; + } + + if (shouldAnimate) { + if (prefersToShow) { + objc_setAssociatedObject(self, &kAssociatedObjectKey_accessoryView, qmuibav_bottomAccessoryView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self addSubview:qmuibav_bottomAccessoryView]; + [self.qmui_backgroundView setNeedsLayout]; + [self.qmui_backgroundView layoutIfNeeded]; + [self setNeedsLayout]; + [self layoutIfNeeded]; + qmuibav_bottomAccessoryView.transform = CGAffineTransformMakeTranslation(0, -CGRectGetHeight(qmuibav_bottomAccessoryView.frame)); + qmuibav_bottomAccessoryView.alpha = 0; + [UIView animateWithDuration:.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + qmuibav_bottomAccessoryView.transform = CGAffineTransformIdentity; + qmuibav_bottomAccessoryView.alpha = 1; + } completion:nil]; + } else { + [UIView animateWithDuration:.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + [self.qmui_backgroundView setNeedsLayout]; + [self.qmui_backgroundView layoutIfNeeded]; + [self setNeedsLayout]; + [self layoutIfNeeded]; + self.qmuibav_bottomAccessoryView.transform = CGAffineTransformMakeTranslation(0, -CGRectGetHeight(self.qmuibav_bottomAccessoryView.frame)); + self.qmuibav_bottomAccessoryView.alpha = 0; + } completion:^(BOOL finished) { + [self.qmuibav_bottomAccessoryView removeFromSuperview]; + self.qmuibav_bottomAccessoryView.transform = CGAffineTransformIdentity; + self.qmuibav_bottomAccessoryView.alpha = 1; + [self.qmuibav_bottomAccessoryView removeFromSuperview]; + objc_setAssociatedObject(self, &kAssociatedObjectKey_accessoryView, qmuibav_bottomAccessoryView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + }]; + } + } else { + // 这个分支包含无动画、有动画但 setter 前和 setter 后都有存在 bottomAccessoryView + if (self.qmuibav_bottomAccessoryView != qmuibav_bottomAccessoryView) { + [self.qmuibav_bottomAccessoryView removeFromSuperview]; + [self addSubview:qmuibav_bottomAccessoryView]; + } + objc_setAssociatedObject(self, &kAssociatedObjectKey_accessoryView, qmuibav_bottomAccessoryView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + [self.qmui_backgroundView setNeedsLayout]; + [self setNeedsLayout]; + } +} + +- (void)setQmuibav_bottomAccessoryView:(__kindof UIView *)qmuibav_bottomAccessoryView { + [self setQmuibav_bottomAccessoryView:qmuibav_bottomAccessoryView animated:NO]; +} + +- (__kindof UIView *)qmuibav_bottomAccessoryView { + return (UIView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_accessoryView); +} + +static char kAssociatedObjectKey_backgroundEffectViewExtendBottomBlock; +- (void)setQmuibav_backgroundEffectViewExtendBottomBlock:(CGFloat (^)(void))qmuibav_backgroundEffectViewExtendBottomBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_backgroundEffectViewExtendBottomBlock, qmuibav_backgroundEffectViewExtendBottomBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + SEL selector = NSSelectorFromString(@"qmuinb_copyStylesToBar"); + NSAssert([self respondsToSelector:selector], @"请检查 UINavigationBar+Transtion 里是否没提供 qmuinb_copyStylesToBar 方法?"); + BeginIgnorePerformSelectorLeaksWarning + UINavigationBar *copyStylesToBar = (UINavigationBar *)[self performSelector:selector]; + EndIgnorePerformSelectorLeaksWarning + if (copyStylesToBar) { + copyStylesToBar.qmuibav_backgroundEffectViewExtendBottomBlock = qmuibav_backgroundEffectViewExtendBottomBlock; + } +} + +- (CGFloat (^)(void))qmuibav_backgroundEffectViewExtendBottomBlock { + return (CGFloat (^)(void))objc_getAssociatedObject(self, &kAssociatedObjectKey_backgroundEffectViewExtendBottomBlock); +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDAlertController.m b/qmuidemo/Modules/Demos/UIKit/QDAlertController.m index 84d99054..3b55557b 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDAlertController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDAlertController.m @@ -21,13 +21,15 @@ - (void)initDataSource { self.dataSource = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: kSectionTitleForAlert, [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: @"显示一个 alert 弹窗", @"", - @"支持 alert 背景磨砂", @"建议用 QMUIVisualEffectView", + @"支持 alert 背景磨砂", @"使用 mainVisualEffectView 指定整个弹窗的磨砂", @"支持自定义 alert 样式", @"支持以 UIAppearance 方式设置全局统一样式", + @"支持添加输入框", @"升起弹窗时会自动聚焦第一个输入框,也可自定义布局", @"支持自定义内容", @"可以将一个 UIView 作为 QMUIAlertController 的 contentView", nil], kSectionTitleForActionSheet, [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: @"显示一个 actionSheet 菜单", @"", - @"支持 actionSheet 背景磨砂", @"建议用 QMUIVisualEffectView", + @"支持按钮分列", @"可左右显示两列", + @"支持 actionSheet 背景磨砂", @"可分别为取消按钮和其他按钮指定不同的磨砂", @"支持自定义 actionSheet 样式", @"支持以 UIAppearance 方式设置全局统一样式", nil], kSectionTitleForSystem, [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: @@ -56,8 +58,8 @@ - (void)didSelectCellWithTitle:(NSString *)title { QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:@"确定删除?" message:@"删除后将无法恢复,请慎重考虑" preferredStyle:QMUIAlertControllerStyleAlert]; [alertController addAction:action1]; [alertController addAction:action2]; - QMUIVisualEffectView *visualEffectView = [[QMUIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; - visualEffectView.foregroundColor = UIColorMakeWithRGBA(255, 255, 255, .7);// 一般用默认值就行,不用主动去改,这里只是为了展示用法 + UIVisualEffectView *visualEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + visualEffectView.qmui_foregroundColor = UIColorMakeWithRGBA(255, 255, 255, .7);// 一般用默认值就行,不用主动去改,这里只是为了展示用法 alertController.mainVisualEffectView = visualEffectView; alertController.alertHeaderBackgroundColor = nil;// 当你需要磨砂的时候请自行去掉这几个背景色,不然这些背景色会盖住磨砂 alertController.alertButtonBackgroundColor = nil; @@ -69,7 +71,7 @@ - (void)didSelectCellWithTitle:(NSString *)title { // 底部按钮 QMUIAlertAction *action1 = [QMUIAlertAction actionWithTitle:@"取消" style:QMUIAlertActionStyleCancel handler:NULL]; QMUIAlertAction *action2 = [QMUIAlertAction actionWithTitle:@"删除" style:QMUIAlertActionStyleDefault handler:NULL]; - [action2.button setImage:[[UIImageMake(@"icon_emotion") qmui_imageResizedInLimitedSize:CGSizeMake(18, 18) resizingMode:QMUIImageResizingModeScaleToFill] qmui_imageWithTintColor:[QDThemeManager sharedInstance].currentTheme.themeTintColor] forState:UIControlStateNormal]; + [action2.button setImage:[[UIImageMake(@"icon_emotion") qmui_imageResizedInLimitedSize:CGSizeMake(18, 18) resizingMode:QMUIImageResizingModeScaleToFill] qmui_imageWithTintColor:UIColor.qd_tintColor] forState:UIControlStateNormal]; action2.button.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 8); // 弹窗 @@ -80,7 +82,7 @@ - (void)didSelectCellWithTitle:(NSString *)title { NSMutableDictionary *messageAttributs = [[NSMutableDictionary alloc] initWithDictionary:alertController.alertMessageAttributes]; messageAttributs[NSForegroundColorAttributeName] = UIColorMakeWithRGBA(255, 255, 255, 0.75); alertController.alertMessageAttributes = messageAttributs; - alertController.alertHeaderBackgroundColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + alertController.alertHeaderBackgroundColor = UIColor.qd_tintColor; alertController.alertSeparatorColor = alertController.alertButtonBackgroundColor; alertController.alertTitleMessageSpacing = 7; @@ -98,6 +100,32 @@ - (void)didSelectCellWithTitle:(NSString *)title { return; } + if ([title isEqualToString:@"支持添加输入框"]) { + QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:@"请输入个人信息" message:@"两项填写一项即可" preferredStyle:QMUIAlertControllerStyleAlert]; + [alertController addAction:[QMUIAlertAction actionWithTitle:@"确定" style:QMUIAlertActionStyleDestructive handler:NULL]]; + [alertController addCancelAction]; + [alertController addTextFieldWithConfigurationHandler:^(QMUITextField * _Nonnull textField) { + textField.placeholder = @"姓"; + }]; + [alertController addTextFieldWithConfigurationHandler:^(QMUITextField * _Nonnull textField) { + textField.placeholder = @"名"; + }]; + + // 输入框的布局默认是贴在一起的,默认不需要修改,这里只是展示可以通过这个 block 自行调整。 + alertController.alertTextFieldMarginBlock = ^UIEdgeInsets(__kindof QMUIAlertController * _Nonnull aAlertController, NSInteger aTextFieldIndex) { + UIEdgeInsets margin = UIEdgeInsetsZero; + if (aTextFieldIndex == aAlertController.textFields.count - 1) { + margin = UIEdgeInsetsSetBottom(margin, 16); + } else { + margin = UIEdgeInsetsSetBottom(margin, 4); + } + return margin; + }; + + [alertController showWithAnimated:YES]; + return; + } + if ([title isEqualToString:@"支持自定义内容"]) { QMUIAlertAction *action1 = [QMUIAlertAction actionWithTitle:@"取消" style:QMUIAlertActionStyleCancel handler:NULL]; QMUIAlertAction *action2 = [QMUIAlertAction actionWithTitle:@"确定" style:QMUIAlertActionStyleDestructive handler:NULL]; @@ -126,6 +154,22 @@ - (void)didSelectCellWithTitle:(NSString *)title { return; } + if ([title isEqualToString:@"支持按钮分列"]) { + QMUIAlertAction *action1 = [QMUIAlertAction actionWithTitle:@"取消" style:QMUIAlertActionStyleCancel handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { + }]; + QMUIAlertAction *action2 = [QMUIAlertAction actionWithTitle:@"删除" style:QMUIAlertActionStyleDestructive handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { + }]; + QMUIAlertAction *action3 = [QMUIAlertAction actionWithTitle:@"确定" style:QMUIAlertActionStyleDefault handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { + }]; + QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:@"确定删除?" message:@"删除后将无法恢复,请慎重考虑" preferredStyle:QMUIAlertControllerStyleActionSheet]; + alertController.sheetButtonColumnCount = 2; + [alertController addAction:action1]; + [alertController addAction:action2]; + [alertController addAction:action3]; + [alertController showWithAnimated:YES]; + return; + } + if ([title isEqualToString:@"支持 actionSheet 背景磨砂"]) { QMUIAlertAction *action1 = [QMUIAlertAction actionWithTitle:@"取消" style:QMUIAlertActionStyleCancel handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { }]; @@ -134,12 +178,12 @@ - (void)didSelectCellWithTitle:(NSString *)title { QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:@"确定删除?" message:@"删除后将无法恢复,请慎重考虑" preferredStyle:QMUIAlertControllerStyleActionSheet]; [alertController addAction:action1]; [alertController addAction:action2]; - QMUIVisualEffectView *visualEffectView = [[QMUIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; - visualEffectView.foregroundColor = UIColorMakeWithRGBA(255, 255, 255, .6);// 一般用默认值就行,不用主动去改,这里只是为了展示用法 + UIVisualEffectView *visualEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + visualEffectView.qmui_foregroundColor = UIColorMakeWithRGBA(255, 255, 255, .6);// 一般用默认值就行,不用主动去改,这里只是为了展示用法 alertController.mainVisualEffectView = visualEffectView;// 这个负责上半部分的磨砂 - visualEffectView = [[QMUIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; - visualEffectView.foregroundColor = UIColorMakeWithRGBA(255, 255, 255, .6);// 一般用默认值就行,不用主动去改,这里只是为了展示用法 + visualEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + visualEffectView.qmui_foregroundColor = UIColorMakeWithRGBA(255, 255, 255, .6);// 一般用默认值就行,不用主动去改,这里只是为了展示用法 alertController.cancelButtonVisualEffectView = visualEffectView;// 这个负责取消按钮的磨砂 alertController.sheetHeaderBackgroundColor = nil; alertController.sheetButtonBackgroundColor = nil; @@ -150,7 +194,7 @@ - (void)didSelectCellWithTitle:(NSString *)title { if ([title isEqualToString:@"支持自定义 actionSheet 样式"]) { QMUIAlertAction *action1 = [QMUIAlertAction actionWithTitle:@"取消" style:QMUIAlertActionStyleCancel handler:NULL]; QMUIAlertAction *action2 = [QMUIAlertAction actionWithTitle:@"删除" style:QMUIAlertActionStyleDefault handler:NULL]; - [action2.button setImage:[[UIImageMake(@"icon_emotion") qmui_imageResizedInLimitedSize:CGSizeMake(22, 22) resizingMode:QMUIImageResizingModeScaleToFill] qmui_imageWithTintColor:[QDThemeManager sharedInstance].currentTheme.themeTintColor] forState:UIControlStateNormal]; + [action2.button setImage:[[UIImageMake(@"icon_emotion") qmui_imageResizedInLimitedSize:CGSizeMake(22, 22) resizingMode:QMUIImageResizingModeScaleToFill] qmui_imageWithTintColor:UIColor.qd_tintColor] forState:UIControlStateNormal]; action2.button.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 8); QMUIAlertController *alertController = [[QMUIAlertController alloc] initWithTitle:@"确定删除?" message:@"删除后将无法恢复,请慎重考虑" preferredStyle:QMUIAlertControllerStyleActionSheet]; NSMutableDictionary *titleAttributs = [[NSMutableDictionary alloc] initWithDictionary:alertController.sheetTitleAttributes]; @@ -159,7 +203,7 @@ - (void)didSelectCellWithTitle:(NSString *)title { NSMutableDictionary *messageAttributs = [[NSMutableDictionary alloc] initWithDictionary:alertController.sheetMessageAttributes]; messageAttributs[NSForegroundColorAttributeName] = UIColorWhite; alertController.sheetMessageAttributes = messageAttributs; - alertController.sheetHeaderBackgroundColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + alertController.sheetHeaderBackgroundColor = UIColor.qd_tintColor; alertController.sheetSeparatorColor = alertController.sheetButtonBackgroundColor; NSMutableDictionary *buttonAttributes = [[NSMutableDictionary alloc] initWithDictionary:alertController.sheetButtonAttributes]; diff --git a/qmuidemo/Modules/Demos/UIKit/QDBlurEffectViewController.h b/qmuidemo/Modules/Demos/UIKit/QDBlurEffectViewController.h new file mode 100644 index 00000000..ef502598 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDBlurEffectViewController.h @@ -0,0 +1,17 @@ +// +// QDBlurEffectViewController.h +// qmuidemo +// +// Created by molice on 2021/11/26. +// Copyright © 2021 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDBlurEffectViewController : QDCommonViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/UIKit/QDBlurEffectViewController.m b/qmuidemo/Modules/Demos/UIKit/QDBlurEffectViewController.m new file mode 100644 index 00000000..a6f8e9f7 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDBlurEffectViewController.m @@ -0,0 +1,151 @@ +// +// QDBlurEffectViewController.m +// qmuidemo +// +// Created by molice on 2021/11/26. +// Copyright © 2021 QMUI Team. All rights reserved. +// + +#import "QDBlurEffectViewController.h" + +@interface QDBlurEffectViewController () + +@property(nonatomic, strong) UIImageView *contentImageView; +@property(nonatomic, strong) UIVisualEffectView *effectView; +@property(nonatomic, strong) UILabel *contentLabel; + +@property(nonatomic, strong) UILabel *label1; +@property(nonatomic, strong) UISlider *slider1; + +@property(nonatomic, strong) UILabel *label2; +@property(nonatomic, strong) QMUIButton *button2; + +@property(nonatomic, strong) UILabel *label3; +@property(nonatomic, strong) QMUIButton *button3; +@property(nonatomic, strong) UIViewPropertyAnimator *animator; +@end + +@implementation QDBlurEffectViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.contentImageView = [[UIImageView alloc] initWithImage:UIImageMake(@"image4")]; + self.contentImageView.contentMode = UIViewContentModeScaleAspectFill; + self.contentImageView.clipsToBounds = YES; + [self.view addSubview:self.contentImageView]; + + self.effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect qmui_effectWithBlurRadius:2]]; + [self.view addSubview:self.effectView]; + + self.contentLabel = [[UILabel alloc] qmui_initWithFont:UIFontBoldMake(32) textColor:UIColorWhite]; + self.contentLabel.textAlignment = NSTextAlignmentCenter; + self.contentLabel.text = @"UIBlurEffect+QMUI"; + self.contentLabel.layer.qmui_shadow = [NSShadow qmui_shadowWithColor:UIColorBlack shadowOffset:CGSizeMake(1, 1) shadowRadius:6]; + [self.contentLabel sizeToFit]; + [self.effectView.contentView addSubview:self.contentLabel]; + + self.label1 = [self generateLabel]; + [self.view addSubview:self.label1]; + + self.slider1 = [self generateSlider]; + self.slider1.minimumValue = 0; + self.slider1.maximumValue = 40; + [self.slider1 addTarget:self action:@selector(handleBlurRadiusChanged:) forControlEvents:UIControlEventValueChanged]; + [self.view addSubview:self.slider1]; + self.slider1.value = 5; + [self.slider1 sendActionsForControlEvents:UIControlEventValueChanged]; + + self.label2 = [self generateLabel]; + self.label2.text = @"2. 支持精确指定磨砂颜色(系统每一种 UIBlurEffectStyle 都会带有默认的前景色,并且无法修改,这会导致我们无法精准设置自己的前景色)"; + [self.view addSubview:self.label2]; + + self.button2 = [QDUIHelper generateLightBorderedButton]; + [self.button2 setTitle:@"点击更换前景色" forState:UIControlStateNormal]; + [self.button2 addTarget:self action:@selector(handleForegroundColorEvent:) forControlEvents:UIControlEventTouchUpInside]; + self.button2.qmui_preventsRepeatedTouchUpInsideEvent = NO;// 为了方便展示效果 + [self.view addSubview:self.button2]; + + self.label3 = [self generateLabel]; + self.label3.text = @"3. 磨砂支持动画(系统能力,这里仅作展示用),也支持配合 UIGestureRecognizer 控制动画进度。"; + [self.view addSubview:self.label3]; + + self.button3 = [QDUIHelper generateLightBorderedButton]; + [self.button3 setTitle:@"开始动画" forState:UIControlStateNormal]; + [self.button3 setTitle:@"动画中..." forState:UIControlStateDisabled]; + [self.button3 addTarget:self action:@selector(handleAnimationEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.button3]; +} + +- (void)dealloc { + [self.animator stopAnimation:YES]; +} + +- (void)handleBlurRadiusChanged:(UISlider *)slider { + CGFloat radius = slider.value; + self.effectView.effect = [UIBlurEffect qmui_effectWithBlurRadius:radius]; + self.label1.text = [NSString stringWithFormat:@"1. 支持精确指定模糊半径(当前%.2f)", radius]; + [self.view setNeedsLayout]; +} + +- (void)handleForegroundColorEvent:(QMUIButton *)button { + UIColor *color = [QDCommonUI.randomThemeColor colorWithAlphaComponent:.3]; + self.effectView.qmui_foregroundColor = color; +} + +- (void)handleAnimationEvent:(QMUIButton *)button { + [self.animator stopAnimation:YES]; + self.effectView.effect = nil; + self.animator = [[UIViewPropertyAnimator alloc] initWithDuration:3 curve:UIViewAnimationCurveEaseInOut animations:^{ + self.effectView.effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; + }]; + __weak __typeof(self)weakSelf = self; + [self.animator addCompletion:^(UIViewAnimatingPosition finalPosition) { + weakSelf.button3.enabled = YES; + }]; + [self.animator startAnimation]; + self.button3.enabled = NO; +} + +- (UILabel *)generateLabel { + UILabel *label = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; + label.qmui_lineHeight = 20; + label.numberOfLines = 0; + return label; +} + +- (UISlider *)generateSlider { + UISlider *slider = [[UISlider alloc] init]; + slider.minimumTrackTintColor = UIColor.qd_tintColor; + slider.maximumTrackTintColor = UIColor.qd_separatorColor; + slider.qmui_trackHeight = 1;// 支持修改背后导轨的高度 + slider.qmui_thumbColor = slider.minimumTrackTintColor; + slider.qmui_thumbSize = CGSizeMake(14, 14); + return slider; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + UIEdgeInsets padding = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator + 24, 24, 24, 24); + CGFloat minY = padding.top; + CGFloat contentWidth = CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding); + + self.contentImageView.frame = CGRectMake(padding.left, minY, contentWidth, 120); + self.effectView.frame = self.contentImageView.frame; + self.contentLabel.center = CGPointMake(CGRectGetWidth(self.effectView.bounds) / 2, CGRectGetHeight(self.effectView.bounds) / 2); + minY = CGRectGetMaxY(self.effectView.frame) + 38; + + self.label1.frame = CGRectMake(padding.left, minY, contentWidth, QMUIViewSelfSizingHeight); + self.slider1.frame = CGRectMake(padding.left, CGRectGetMaxY(self.label1.frame) + 16, contentWidth, QMUIViewSelfSizingHeight); + minY = CGRectGetMaxY(self.slider1.frame) + 38; + + self.label2.frame = CGRectMake(padding.left, minY, contentWidth, QMUIViewSelfSizingHeight); + self.button2.frame = CGRectMake(padding.left, CGRectGetMaxY(self.label2.frame) + 16, contentWidth, CGRectGetHeight(self.button2.frame)); + minY = CGRectGetMaxY(self.button2.frame) + 38; + + self.label3.frame = CGRectMake(padding.left, minY, contentWidth, QMUIViewSelfSizingHeight); + self.button3.frame = CGRectMake(padding.left, CGRectGetMaxY(self.label3.frame) + 16, contentWidth, CGRectGetHeight(self.button3.frame)); + minY = CGRectGetMaxY(self.button3.frame) + 38; +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDButtonEdgeInsetsViewController.m b/qmuidemo/Modules/Demos/UIKit/QDButtonEdgeInsetsViewController.m index 840fbeee..2244a4e6 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDButtonEdgeInsetsViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDButtonEdgeInsetsViewController.m @@ -82,6 +82,13 @@ - (__kindof UIButton *)generateButtonWithClass:(Class)buttonClass { button.titleLabel.layer.borderColor = UIColorRed.CGColor; [button addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; [self.scrollView addSubview:button]; + if ([button isKindOfClass:QMUIButton.class]) { + QMUIButton *b = (QMUIButton *)button; + b.subtitleLabel.font = UIFontMake(14); + b.subtitleLabel.textColor = UIColorWhite; + b.subtitleLabel.layer.borderWidth = PixelOne; + b.subtitleLabel.layer.borderColor = UIColorRed.CGColor; + } return button; } @@ -90,10 +97,14 @@ - (void)copyButtonPropertyFromButton:(UIButton *)fromButton toButton:(UIButton * toButton.contentEdgeInsets = fromButton.contentEdgeInsets; toButton.imageEdgeInsets = fromButton.imageEdgeInsets; toButton.titleEdgeInsets = fromButton.titleEdgeInsets; + if ([toButton isKindOfClass:QMUIButton.class]) { + QMUIButton *b = (QMUIButton *)toButton; + b.subtitleEdgeInsets = fromButton.titleEdgeInsets; + } } - (UILabel *)generateLabelWithTitle:(NSString *)title { - UILabel *label = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorGray3]; + UILabel *label = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; label.numberOfLines = 0; label.qmui_lineHeight = 20; label.text = title; @@ -149,7 +160,7 @@ - (void)handleButtonEvent:(UIButton *)button { #pragma mark - -- (UIImage *)navigationBarBackgroundImage { +- (UIImage *)qmui_navigationBarBackgroundImage { // debug warning return [UIImage qmui_imageWithColor:UIColorMake(232, 46, 46)]; } @@ -165,6 +176,10 @@ @implementation QDButtonConfigurePopupView { UISwitch *_shouldShowTitleSwitch; CALayer *_shouldShowTitleSeparatorLayer; + UILabel *_shouldShowSubtitleLabel; + UISwitch *_shouldShowSubtitleSwitch; + CALayer *_shouldShowSubtitleSeparatorLayer; + UILabel *_bigImageLabel; UISwitch *_bigImageSwitch; CALayer *_bigImageSeparatorLayer; @@ -203,6 +218,12 @@ - (void)didInitialize { _shouldShowTitleSeparatorLayer = [CALayer qmui_separatorLayer]; [self.contentView.layer addSublayer:_shouldShowTitleSeparatorLayer]; + // 是否要显示副标题 + _shouldShowSubtitleLabel = [self generateLabelWithText:@"setSubtitle"]; + _shouldShowSubtitleSwitch = [self generateSwitch]; + _shouldShowSubtitleSeparatorLayer = [CALayer qmui_separatorLayer]; + [self.contentView.layer addSublayer:_shouldShowSubtitleSeparatorLayer]; + // 是否要显示超大图片 _bigImageLabel = [self generateLabelWithText:@"bigImage"]; _bigImageSwitch = [self generateSwitch]; @@ -235,7 +256,8 @@ - (void)didInitialize { } - (CGSize)sizeThatFitsInContentView:(CGSize)size { - return CGSizeMake(size.width, 315); + NSInteger rowCount = 8; + return CGSizeMake(size.width, (45 + PixelOne * 2) * rowCount); } - (void)layoutSubviews { @@ -254,6 +276,11 @@ - (void)layoutSubviews { _shouldShowTitleSeparatorLayer.frame = CGRectSetY(_shouldShowImageSeparatorLayer.frame, CGRectGetMaxY(_shouldShowTitleLabel.frame) + 14); minY = ceil(CGRectGetMaxY(_shouldShowTitleSeparatorLayer.frame)); + _shouldShowSubtitleLabel.frame = CGRectSetY(_shouldShowSubtitleLabel.frame, minY + 14); + _shouldShowSubtitleSwitch.center = CGPointMake(switchCenter.x, _shouldShowSubtitleLabel.center.y); + _shouldShowSubtitleSeparatorLayer.frame = CGRectSetY(_shouldShowImageSeparatorLayer.frame, CGRectGetMaxY(_shouldShowSubtitleLabel.frame) + 14); + minY = ceil(CGRectGetMaxY(_shouldShowSubtitleSeparatorLayer.frame)); + _bigImageLabel.frame = CGRectSetY(_bigImageLabel.frame, minY + 14); _bigImageSwitch.center = CGPointMake(switchCenter.x, _bigImageLabel.center.y); _bigImageSeparatorLayer.frame = CGRectSetY(_shouldShowImageSeparatorLayer.frame, CGRectGetMaxY(_bigImageLabel.frame) + 14); @@ -281,7 +308,7 @@ - (void)layoutSubviews { } - (UILabel *)generateLabelWithText:(NSString *)text { - UILabel *label = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorGray3]; + UILabel *label = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; label.text = text; [label sizeToFit]; [self.contentView addSubview:label]; @@ -290,7 +317,6 @@ - (UILabel *)generateLabelWithText:(NSString *)text { - (UISwitch *)generateSwitch { UISwitch *switchControl = [[UISwitch alloc] init]; - switchControl.tintColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; [switchControl sizeToFit]; switchControl.transform = CGAffineTransformMakeScale(.7, .7); [switchControl addTarget:self action:@selector(handleSwitchControlEvent:) forControlEvents:UIControlEventValueChanged]; @@ -300,7 +326,7 @@ - (UISwitch *)generateSwitch { - (UISegmentedControl *)generateSegmentedWithItems:(NSArray *)items { UISegmentedControl *segmentedControl = [[UISegmentedControl alloc] initWithItems:items]; - segmentedControl.tintColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + segmentedControl.tintColor = UIColor.qd_tintColor; segmentedControl.frame = CGRectSetWidth(segmentedControl.frame, 240);// 统一按照最长的来就行啦 segmentedControl.transform = CGAffineTransformMakeScale(.8, .8); [segmentedControl addTarget:self action:@selector(handleSegmentedControlEvent:) forControlEvents:UIControlEventValueChanged]; @@ -312,6 +338,8 @@ - (void)setBindButton:(UIButton *)bindButton { _bindButton = bindButton; _shouldShowImageSwitch.on = !!bindButton.currentImage; _shouldShowTitleSwitch.on = !!bindButton.currentTitle; + _shouldShowSubtitleSwitch.enabled = [bindButton isKindOfClass:QMUIButton.class]; + _shouldShowSubtitleSwitch.on = _shouldShowSubtitleSwitch.enabled && ((QMUIButton *)bindButton).subtitle.length; _bigImageSwitch.on = bindButton.currentImage.size.width >= 80; _longTitleSwitch.on = bindButton.currentTitle.length >= 20; _imagePositionSegmented.enabled = [bindButton isKindOfClass:[QMUIButton class]]; @@ -327,6 +355,11 @@ - (void)handleSwitchControlEvent:(UISwitch *)switchControl { if (switchControl == _shouldShowTitleSwitch || switchControl == _longTitleSwitch) { [self updateTitleForButton:self.bindButton shouldShowTitle:_shouldShowTitleSwitch.on shouldShowLongTitle:_longTitleSwitch.on]; } + if (switchControl == _shouldShowSubtitleSwitch || switchControl == _longTitleSwitch) { + if ([self.bindButton isKindOfClass:QMUIButton.class]) { + [self updateSubtitleForButton:(QMUIButton *)self.bindButton shouldShowSubtitle:_shouldShowSubtitleSwitch.on shouldShowLongTitle:_longTitleSwitch.on]; + } + } [self updateLayoutForButton:self.bindButton]; } @@ -361,6 +394,15 @@ - (void)updateTitleForButton:(UIButton *)button shouldShowTitle:(BOOL)shouldShow } } +- (void)updateSubtitleForButton:(QMUIButton *)button shouldShowSubtitle:(BOOL)shouldShowSubtitle shouldShowLongTitle:(BOOL)shouldShowLongTitle { + if (!shouldShowSubtitle) { + button.subtitle = nil; + } else { + NSString *subtitle = shouldShowLongTitle ? @"很长很长的标题很长很长的标题很长很长的副标题" : @"副标题"; + button.subtitle = subtitle; + } +} + - (void)updateLayoutForButton:(UIButton *)button { if (button.tag != TagForStaticSizeView) { [button sizeToFit]; diff --git a/qmuidemo/Modules/Demos/UIKit/QDButtonViewController.m b/qmuidemo/Modules/Demos/UIKit/QDButtonViewController.m index beb4396c..22fc61f0 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDButtonViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDButtonViewController.m @@ -8,9 +8,6 @@ #import "QDButtonViewController.h" #import "QDNormalButtonViewController.h" -#import "QDLinkButtonViewController.h" -#import "QDGhostButtonViewController.h" -#import "QDFillButtonViewController.h" #import "QDNavigationButtonViewController.h" #import "QDToolBarButtonViewController.h" #import "QDButtonEdgeInsetsViewController.h" @@ -20,9 +17,6 @@ @implementation QDButtonViewController - (void)initDataSource { self.dataSource = @[@"QMUIButton", - @"QMUILinkButton", - @"QMUIGhostButton", - @"QMUIFillButton", @"QMUINavigationButton", @"QMUIToolbarButton"]; } @@ -31,12 +25,6 @@ - (void)didSelectCellWithTitle:(NSString *)title { UIViewController *viewController = nil; if ([title isEqualToString:@"QMUIButton"]) { viewController = [[QDNormalButtonViewController alloc] init]; - } else if ([title isEqualToString:@"QMUILinkButton"]) { - viewController = [[QDLinkButtonViewController alloc] init]; - } else if ([title isEqualToString:@"QMUIGhostButton"]) { - viewController = [[QDGhostButtonViewController alloc] init]; - } else if ([title isEqualToString:@"QMUIFillButton"]) { - viewController = [[QDFillButtonViewController alloc] init]; } else if ([title isEqualToString:@"QMUINavigationButton"]) { viewController = [[QDNavigationButtonViewController alloc] init]; } else if ([title isEqualToString:@"QMUIToolbarButton"]) { diff --git a/qmuidemo/Modules/Demos/UIKit/QDCAAnimationViewController.m b/qmuidemo/Modules/Demos/UIKit/QDCAAnimationViewController.m index 6b2d8549..4adbe4f5 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDCAAnimationViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDCAAnimationViewController.m @@ -27,7 +27,7 @@ - (void)initSubviews { self.tipsLabel = [[UILabel alloc] init]; self.tipsLabel.numberOfLines = 0; - NSMutableAttributedString *tips = [[NSMutableAttributedString alloc] initWithString:@"CAAnimation (QMUI) 支持用 block 的形式添加对 animationDidStart 和 animationDidStop 的监听,无需自行设置 delegate,从而避免 CAAnimation.delegate 为 strong 带来的一些内存管理上的麻烦。\n同时你也可以继续使用系统原有的 delegate 方法,互不影响。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColorGray6, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]}]; + NSMutableAttributedString *tips = [[NSMutableAttributedString alloc] initWithString:@"CAAnimation (QMUI) 支持用 block 的形式添加对 animationDidStart 和 animationDidStop 的监听,无需自行设置 delegate,从而避免 CAAnimation.delegate 为 strong 带来的一些内存管理上的麻烦。\n同时你也可以继续使用系统原有的 delegate 方法,互不影响。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]}]; NSDictionary *codeAttributes = CodeAttributes(12); [tips.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { [tips addAttributes:codeAttributes range:codeRange]; @@ -38,7 +38,7 @@ - (void)initSubviews { self.layer = [CALayer layer]; [self.layer qmui_removeDefaultAnimations]; self.layer.cornerRadius = self.actionButton.layer.cornerRadius; - self.layer.backgroundColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor.CGColor; + self.layer.backgroundColor = UIColor.qd_tintColor.CGColor; [self.view.layer addSublayer:self.layer]; } @@ -68,7 +68,7 @@ - (void)handleActionButtonEvent:(QMUIButton *)button { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - UIEdgeInsets paddings = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.qmui_safeAreaInsets.left, 24 + self.view.qmui_safeAreaInsets.bottom, 24 + self.view.qmui_safeAreaInsets.right); + UIEdgeInsets paddings = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.safeAreaInsets.left, 24 + self.view.safeAreaInsets.bottom, 24 + self.view.safeAreaInsets.right); self.layer.frame = CGRectMake(paddings.left, paddings.top, 64, 64); self.actionButton.frame = CGRectMake(paddings.left, CGRectGetMaxY(self.layer.frame) + 24, CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(paddings), CGRectGetHeight(self.actionButton.frame)); self.tipsLabel.frame = CGRectMake(paddings.left, CGRectGetMaxY(self.actionButton.frame) + 16, CGRectGetWidth(self.actionButton.frame), QMUIViewSelfSizingHeight); diff --git a/qmuidemo/Modules/Demos/UIKit/QDChangeNavBarStyleViewController.h b/qmuidemo/Modules/Demos/UIKit/QDChangeNavBarStyleViewController.h index ebe79ecd..982ce536 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDChangeNavBarStyleViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDChangeNavBarStyleViewController.h @@ -16,8 +16,6 @@ typedef NS_ENUM(NSInteger, QDNavigationBarStyle) { @interface QDChangeNavBarStyleViewController : QDCommonListViewController -@property(nonatomic, assign) QDNavigationBarStyle previousBarStyle; -@property(nonatomic, assign) BOOL customNavBarTransition; - (instancetype)initWithBarStyle:(QDNavigationBarStyle)barStyle; @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDChangeNavBarStyleViewController.m b/qmuidemo/Modules/Demos/UIKit/QDChangeNavBarStyleViewController.m index 5ac17f34..5d4efcdb 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDChangeNavBarStyleViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDChangeNavBarStyleViewController.m @@ -11,7 +11,6 @@ @interface QDChangeNavBarStyleViewController () @property(nonatomic, assign) QDNavigationBarStyle barStyle; -@property(nonatomic, strong) QDChangeNavBarStyleViewController *viewController; @end @@ -26,27 +25,24 @@ - (instancetype)initWithBarStyle:(QDNavigationBarStyle)barStyle { - (void)initDataSource { [super initDataSource]; - self.dataSource = @[@"默认navBar样式", - @"暗色navBar样式", - @"浅色navBar样式"]; + self.dataSource = @[@"默认", + @"深色", + @"浅色(磨砂)"]; } - (void)didSelectCellWithTitle:(NSString *)title { - if ([title isEqualToString:@"默认navBar样式"]) { - self.viewController = [[QDChangeNavBarStyleViewController alloc] initWithBarStyle:QDNavigationBarStyleOrigin]; + UIViewController *viewController = nil; + if ([title isEqualToString:@"默认"]) { + viewController = [[QDChangeNavBarStyleViewController alloc] initWithBarStyle:QDNavigationBarStyleOrigin]; } - else if ([title isEqualToString:@"暗色navBar样式"]) { - self.viewController = [[QDChangeNavBarStyleViewController alloc] initWithBarStyle:QDNavigationBarStyleDark]; + else if ([title isEqualToString:@"深色"]) { + viewController = [[QDChangeNavBarStyleViewController alloc] initWithBarStyle:QDNavigationBarStyleDark]; } - else if ([title isEqualToString:@"浅色navBar样式"]) { - self.viewController = [[QDChangeNavBarStyleViewController alloc] initWithBarStyle:QDNavigationBarStyleLight]; + else if ([title isEqualToString:@"浅色(磨砂)"]) { + viewController = [[QDChangeNavBarStyleViewController alloc] initWithBarStyle:QDNavigationBarStyleLight]; } - if (self.customNavBarTransition) { - self.viewController.previousBarStyle = self.barStyle; - self.viewController.customNavBarTransition = YES; - } - self.viewController.title = title; - [self.navigationController pushViewController:self.viewController animated:YES]; + viewController.title = title; + [self.navigationController pushViewController:viewController animated:YES]; } - (UIStatusBarStyle)preferredStatusBarStyle { @@ -59,7 +55,7 @@ - (UIStatusBarStyle)preferredStatusBarStyle { #pragma mark - QMUINavigationControllerDelegate -- (UIImage *)navigationBarBackgroundImage { +- (UIImage *)qmui_navigationBarBackgroundImage { if (self.barStyle == QDNavigationBarStyleOrigin) { return NavBarBackgroundImage; } else if (self.barStyle == QDNavigationBarStyleLight) { @@ -71,40 +67,27 @@ - (UIImage *)navigationBarBackgroundImage { } } -- (UIImage *)navigationBarShadowImage { - if (self.barStyle == QDNavigationBarStyleOrigin) { - return NavBarShadowImage; - } else if (self.barStyle == QDNavigationBarStyleLight) { +- (UIImage *)qmui_navigationBarShadowImage { + if (self.barStyle == QDNavigationBarStyleLight) { return nil; // nil则用系统默认颜色 - } else if (self.barStyle == QDNavigationBarStyleDark) { - return [UIImage qmui_imageWithColor:UIColorMake(99, 99, 99) size:CGSizeMake(10, PixelOne) cornerRadius:0]; - } else { - return NavBarShadowImage; } + return NavBarShadowImage; } -- (UIColor *)navigationBarTintColor { +- (UIColor *)qmui_navigationBarTintColor { if (self.barStyle == QDNavigationBarStyleOrigin) { return NavBarTintColor; } else if (self.barStyle == QDNavigationBarStyleLight) { - return UIColorBlue; + return UIColorBlack; } else if (self.barStyle == QDNavigationBarStyleDark) { - return NavBarTintColor; + return UIColorWhite; } else { return NavBarTintColor; } } -- (UIColor *)titleViewTintColor { - if (self.barStyle == QDNavigationBarStyleOrigin) { - return NavBarTitleColor; - } else if (self.barStyle == QDNavigationBarStyleLight) { - return UIColorBlack; - } else if (self.barStyle == QDNavigationBarStyleDark) { - return UIColorWhite; - } else { - return NavBarTitleColor; - } +- (UIColor *)qmui_titleViewTintColor { + return [self qmui_navigationBarTintColor]; } #pragma mark - diff --git a/qmuidemo/Modules/Demos/UIKit/QDColorViewController.h b/qmuidemo/Modules/Demos/UIKit/QDColorViewController.h index 18d24436..785a0d21 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDColorViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDColorViewController.h @@ -50,6 +50,10 @@ @interface QDColorCellThatBlendColors : QDColorTableViewCell @end +// 计算两个颜色之间的差距 +@interface QDColorCellThatGetDistance : QDColorTableViewCell +@end + @interface QDColorCellThatAdjustAlphaAndBlend : QDColorTableViewCell @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDColorViewController.m b/qmuidemo/Modules/Demos/UIKit/QDColorViewController.m index 2a60f3ed..dbddf0e6 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDColorViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDColorViewController.m @@ -29,7 +29,7 @@ - (void)initTableView { #pragma mark - TableView Delegate & DataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return 7; + return 8; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { @@ -63,6 +63,8 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N case 6: cell = [[QDColorCellThatAdjustAlphaAndBlend alloc] init]; break; + case 7: + cell = [[QDColorCellThatGetDistance alloc] init]; default: break; } @@ -72,8 +74,6 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N return cell; } - - @end @@ -83,6 +83,7 @@ - (void)didInitializeWithStyle:(UITableViewCellStyle)style { [super didInitializeWithStyle:style]; self.titleLabelMarginBottom = 12; self.titleLabel = [[QMUILabel alloc] init]; + self.titleLabel.textColor = TableViewCellTitleLabelColor; self.titleLabel.font = UIFontMake(14); [self.titleLabel qmui_calculateHeightAfterSetAppearance]; [self.contentView addSubview:self.titleLabel]; @@ -100,10 +101,15 @@ - (void)setContentViewInsets:(UIEdgeInsets)contentViewInsets { } // 生成一个圆形的view -- (UIView *)generateCircleWithColor:(UIColor *)color { +- (__kindof UIView *)generateCircleWithColor:(UIColor *)color prefersButton:(BOOL)prefersButton { CGFloat diameter = 44; - UIView *circle = [[UIView alloc] init]; + UIView *circle = nil; + if (prefersButton) { + circle = [[QMUIButton alloc] init]; + } else { + circle = [[UIView alloc] init]; + } circle.backgroundColor = color; circle.frame = CGRectMake(0, 0, diameter, diameter); circle.layer.cornerRadius = diameter / 2; @@ -111,6 +117,10 @@ - (UIView *)generateCircleWithColor:(UIColor *)color { return circle; } +- (UIView *)generateCircleWithColor:(UIColor *)color { + return [self generateCircleWithColor:color prefersButton:NO]; +} + // 生成一个向右的箭头imageView - (UIImageView *)generateArrowIcon { UIImageView *imageView = [[UIImageView alloc] initWithImage:UIImageMake(@"arrowRight")]; @@ -143,7 +153,7 @@ - (void)didInitializeWithStyle:(UITableViewCellStyle)style { _label.text = @"[UIColor qmui_colorWithHexString:@\"#cddc39\"]"; _label.font = UIFontMake(12); [_label sizeToFit]; - _label.textColor = UIColorGray7; + _label.textColor = UIColor.qd_descriptionTextColor; [self.contentView addSubview:_label]; } @@ -203,7 +213,7 @@ - (void)didInitializeWithStyle:(UITableViewCellStyle)style { QMUILabel *titleLabel = [[QMUILabel alloc] init]; titleLabel.text = dict[@"title"]; titleLabel.font = UIFontMake(12); - titleLabel.textColor = UIColorGray7; + titleLabel.textColor = UIColor.qd_descriptionTextColor; titleLabel.textAlignment = NSTextAlignmentCenter; [titleLabel sizeToFit]; [_labels addObject:titleLabel]; @@ -212,7 +222,7 @@ - (void)didInitializeWithStyle:(UITableViewCellStyle)style { QMUILabel *contentLabel = [[QMUILabel alloc] init]; contentLabel.text = dict[@"content"]; contentLabel.font = UIFontMake(12); - contentLabel.textColor = UIColorGray3; + contentLabel.textColor = UIColor.qd_mainTextColor; contentLabel.textAlignment = NSTextAlignmentCenter; [contentLabel sizeToFit]; contentLabel.frame = CGRectSetY(contentLabel.frame, CGRectGetMaxY(titleLabel.frame) + 3); @@ -267,7 +277,7 @@ - (void)didInitializeWithStyle:(UITableViewCellStyle)style { _label1 = [[QMUILabel alloc] init]; _label1.text = @"0.5 ALPHA"; _label1.font = UIFontMake(12); - _label1.textColor = UIColorGray7; + _label1.textColor = UIColor.qd_descriptionTextColor; [_label1 sizeToFit]; [self.contentView addSubview:_label1]; @@ -411,6 +421,85 @@ - (void)layoutSubviews { } @end +@interface QDColorCellThatGetDistance () +@end + +@implementation QDColorCellThatGetDistance { + QMUIButton *_circle1; + QMUIButton *_circle2; + UIView *_arrow; + QMUILabel *_label; +} + +- (void)didInitializeWithStyle:(UITableViewCellStyle)style { + [super didInitializeWithStyle:style]; + + self.titleLabel.text = @"计算两色的相似程度"; + + UIColor *rawColor1 = UIColorMakeWithHex(@"#ff9800"); + UIColor *rawColor2 = [rawColor1 qmui_inverseColor]; + CGFloat distance = [rawColor1 qmui_distanceBetweenColor:rawColor2];// 关键方法 + + _circle1 = [self generateCircleWithColor:rawColor1 prefersButton:YES]; + [_circle1 addTarget:self action:@selector(handleColorPicker:) forControlEvents:UIControlEventTouchUpInside]; + [self.contentView addSubview:_circle1]; + + _circle2 = [self generateCircleWithColor:rawColor2 prefersButton:YES]; + [_circle2 addTarget:self action:@selector(handleColorPicker:) forControlEvents:UIControlEventTouchUpInside]; + [self.contentView addSubview:_circle2]; + + _arrow = [self generateArrowIcon]; + [self.contentView addSubview:_arrow]; + + _label = [[QMUILabel alloc] init]; + _label.text = [NSString stringWithFormat:@"%.2f(0表示相等,值越大差距越大)", distance]; + _label.textColor = UIColor.qd_descriptionTextColor; + _label.font = UIFontMake(12); + [self.contentView addSubview:_label]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + _circle1.frame = CGRectSetXY(_circle1.frame, self.contentViewInsets.left, CGRectGetMaxY(self.titleLabel.frame) + self.titleLabelMarginBottom); + _circle2.frame = CGRectSetXY(_circle2.frame, CGRectGetMidX(_circle1.frame), CGRectGetMinY(_circle1.frame)); + _arrow.frame = CGRectSetXY(_arrow.frame, CGRectGetMaxX(_circle2.frame) + spaceBetweenIconAndCircle, CGRectGetMinYVerticallyCenter(_circle2.frame, _arrow.frame)); + [_label sizeToFit]; + _label.frame = CGRectSetXY(_label.frame, CGRectGetMaxX(_arrow.frame) + spaceBetweenIconAndCircle, CGRectGetMinYVerticallyCenter(_circle1.frame, _label.frame)); +} + +- (void)handleColorPicker:(QMUIButton *)button { + if (@available(iOS 14.0, *)) { + button.selected = YES; + if (button == _circle1) { + _circle2.selected = NO; + } else { + _circle1.selected = NO; + } + UIColorPickerViewController *vc = [[UIColorPickerViewController alloc] init]; + vc.delegate = self; + [button.qmui_viewController presentViewController:vc animated:YES completion:nil]; + } +} + +#pragma mark - + +BeginIgnoreAvailabilityWarning +- (void)colorPickerViewControllerDidSelectColor:(UIColorPickerViewController *)viewController { + UIView *view = nil; + if (_circle1.selected) { + view = _circle1; + } else { + view = _circle2; + } + view.backgroundColor = viewController.selectedColor; + CGFloat distance = [_circle1.backgroundColor qmui_distanceBetweenColor:_circle2.backgroundColor];// 关键方法 + _label.text = [NSString stringWithFormat:@"%.2f(0表示相等,值越大差距越大)", distance]; + [self setNeedsLayout]; +} +EndIgnoreAvailabilityWarning + +@end + @implementation QDColorCellThatAdjustAlphaAndBlend { UIView *_circle1; UIView *_circle2; @@ -438,7 +527,7 @@ - (void)didInitializeWithStyle:(UITableViewCellStyle)style { _label = [[QMUILabel alloc] init]; _label.text = @"0.5 ALPHA"; - _label.textColor = UIColorGray7;; + _label.textColor = UIColor.qd_descriptionTextColor; _label.font = UIFontMake(12); [_label sizeToFit]; diff --git a/qmuidemo/Modules/Demos/UIKit/QDControlViewController.h b/qmuidemo/Modules/Demos/UIKit/QDControlViewController.h new file mode 100644 index 00000000..d50a69e1 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDControlViewController.h @@ -0,0 +1,17 @@ +// +// QDControlViewController.h +// qmuidemo +// +// Created by MoLice on 2020/9/2. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDControlViewController : QDCommonViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/UIKit/QDControlViewController.m b/qmuidemo/Modules/Demos/UIKit/QDControlViewController.m new file mode 100644 index 00000000..ba51b1a7 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDControlViewController.m @@ -0,0 +1,91 @@ +// +// QDControlViewController.m +// qmuidemo +// +// Created by MoLice on 2020/9/2. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDControlViewController.h" + +@interface QDControlViewController () + +@property(nonatomic, strong) UIScrollView *scrollView; +@property(nonatomic, strong) QMUIButton *button1; +@property(nonatomic, strong) QMUIButton *button2; +@property(nonatomic, strong) UILabel *tipsLabel1; + +@property(nonatomic, strong) QMUIButton *button3; +@property(nonatomic, strong) UILabel *tipsLabel2; +@end + +@implementation QDControlViewController + +- (void)initSubviews { + [super initSubviews]; + self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; + [self.view addSubview:self.scrollView]; + self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + + self.button1 = [QDUIHelper generateLightBorderedButton]; + [self.button1 setImage:UIImageMake(@"icon_emotion") forState:UIControlStateNormal]; + [self.scrollView addSubview:self.button1]; + + self.button2 = [QDUIHelper generateLightBorderedButton]; + [self.button2 setImage:UIImageMake(@"icon_emotion") forState:UIControlStateNormal]; + self.button2.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES;// 开启 qmui_automaticallyAdjustTouchHighlightedInScrollView 令其在 UIScrollView 内也有快速点击时的高亮效果 + [self.scrollView addSubview:self.button2]; + + self.tipsLabel1 = [[UILabel alloc] init]; + self.tipsLabel1.attributedText = ({ + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"放在 UIScrollView 上的 UIControl 点击时会有 300ms 的延迟,所以当你点击左边按钮并快速抬起手时将无法看到点击效果,但右边的按钮开启了 qmui_automaticallyAdjustTouchHighlightedInScrollView,快速点击能看到点击效果。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]}]; + NSDictionary *codeAttributes = CodeAttributes(12); + [attributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { + [attributedString addAttributes:codeAttributes range:codeRange]; + }]; + attributedString; + }); + self.tipsLabel1.numberOfLines = 0; + [self.scrollView addSubview:self.tipsLabel1]; + + self.button3 = [QDUIHelper generateLightBorderedButton]; + [self.button3 setTitle:@"0" forState:UIControlStateNormal]; + self.button3.qmui_preventsRepeatedTouchUpInsideEvent = YES;// 开启防重复点击,QMUI Demo 里对 QMUIButton 默认就是打开的,这句代码只是示例作用。 + [self.button3 addTarget:self action:@selector(handleButton3Event:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.button3]; + + self.tipsLabel2 = [[UILabel alloc] init]; + self.tipsLabel2.attributedText = ({ + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"开启 qmui_preventsRepeatedTouchUpInsideEvent 后可防止误操作带来的重复点击,你可以试试快速点击上方的按钮,只有当界面静止超过 300ms 后,下一次点击才会重新触发 UIControlEventTouchUpInside。\nQMUI Demo 默认对导航栏的 UIBarButtonItem 开启了防重复点击效果,业务项目如果需要,可以参照 QMUI Demo 的写法,具体可以全局搜索 “qmui_preventsRepeatedTouchUpInsideEvent = YES”。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]}]; + NSDictionary *codeAttributes = CodeAttributes(12); + [attributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { + [attributedString addAttributes:codeAttributes range:codeRange]; + }]; + attributedString; + }); + self.tipsLabel2.numberOfLines = 0; + [self.view addSubview:self.tipsLabel2]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + + UIEdgeInsets padding = UIEdgeInsetsMake(24, 16 + self.view.safeAreaInsets.left, 16 + self.view.safeAreaInsets.bottom, 16 + self.view.safeAreaInsets.right); + CGFloat contentWidth = CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding); + + CGFloat buttonWidth = 80; + CGFloat buttonMinX = flat((CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(self.view.safeAreaInsets) - buttonWidth * 2) / 3.0); + self.button1.frame = CGRectMake(buttonMinX, padding.top, 80, CGRectGetHeight(self.button1.frame)); + self.button2.frame = CGRectMake(CGRectGetWidth(self.view.bounds) - buttonMinX - buttonWidth, CGRectGetMinY(self.button1.frame), buttonWidth, CGRectGetHeight(self.button2.frame)); + self.tipsLabel1.frame = CGRectMake(padding.left, CGRectGetMaxY(self.button1.frame) + 16, contentWidth, QMUIViewSelfSizingHeight); + self.scrollView.frame = CGRectMake(0, self.qmui_navigationBarMaxYInViewCoordinator, CGRectGetWidth(self.view.bounds), CGRectGetMaxY(self.tipsLabel1.frame) + 16); + + self.button3.frame = CGRectMake(padding.left, CGRectGetMaxY(self.scrollView.frame) + 44, contentWidth, CGRectGetHeight(self.button3.frame)); + self.tipsLabel2.frame = CGRectMake(padding.left, CGRectGetMaxY(self.button3.frame) + 16, contentWidth, QMUIViewSelfSizingHeight); +} + +- (void)handleButton3Event:(QMUIButton *)button { + [button setTitle:[NSString qmui_stringWithNSInteger:button.currentTitle.integerValue + 1] forState:UIControlStateNormal]; +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDFillButtonViewController.h b/qmuidemo/Modules/Demos/UIKit/QDFillButtonViewController.h deleted file mode 100644 index 435fb1c8..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDFillButtonViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDFillButtonViewController.h -// qmuidemo -// -// Created by QMUI Team on 15/5/23. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QDCommonViewController.h" - -@interface QDFillButtonViewController : QDCommonViewController - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDFillButtonViewController.m b/qmuidemo/Modules/Demos/UIKit/QDFillButtonViewController.m deleted file mode 100644 index 2722cf6b..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDFillButtonViewController.m +++ /dev/null @@ -1,83 +0,0 @@ -// -// QDFillButtonViewController.m -// qmuidemo -// -// Created by QMUI Team on 15/5/23. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QDFillButtonViewController.h" - -@interface QDFillButtonViewController () - -@property(nonatomic, strong) QMUIFillButton *fillButton1; -@property(nonatomic, strong) QMUIFillButton *fillButton2; -@property(nonatomic, strong) QMUIFillButton *fillButton3; - -@property(nonatomic, strong) CALayer *separatorLayer1; -@property(nonatomic, strong) CALayer *separatorLayer2; -@property(nonatomic, strong) CALayer *separatorLayer3; - -@end - -@implementation QDFillButtonViewController - -- (void)initSubviews { - [super initSubviews]; - - _fillButton1 = [[QMUIFillButton alloc] initWithFillType:QMUIFillButtonColorBlue]; - self.fillButton1.titleLabel.font = UIFontMake(14); - [self.fillButton1 setTitle:@"QMUIFillButtonColorBlue" forState:UIControlStateNormal]; - [self.view addSubview:self.fillButton1]; - - _fillButton2 = [[QMUIFillButton alloc] initWithFillType:QMUIFillButtonColorRed]; - self.fillButton2.titleLabel.font = UIFontMake(14); - // 默认点击态是半透明处理,如果需要点击态是其他颜色,修改下面两个属性 - // self.fillButton2.adjustsButtonWhenHighlighted = NO; - // self.fillButton2.highlightedBackgroundColor = UIColorMake(70, 160, 242); - [self.fillButton2 setTitle:@"QMUIFillButtonColorRed" forState:UIControlStateNormal]; - [self.view addSubview:self.fillButton2]; - - _fillButton3 = [[QMUIFillButton alloc] initWithFillType:QMUIFillButtonColorGreen]; - self.fillButton3.titleLabel.font = UIFontMake(14); - [self.fillButton3 setTitle:@"点击修改按钮fillColor" forState:UIControlStateNormal]; - [self.fillButton3 setImage:UIImageMake(@"icon_emotion") forState:UIControlStateNormal]; - self.fillButton3.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 6); - self.fillButton3.adjustsImageWithTitleTextColor = YES; - [self.fillButton3 addTarget:self action:@selector(handleFillButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self.view addSubview:self.fillButton3]; - - self.separatorLayer1 = [QDCommonUI generateSeparatorLayer]; - [self.view.layer addSublayer:self.separatorLayer1]; - - self.separatorLayer2 = [QDCommonUI generateSeparatorLayer]; - [self.view.layer addSublayer:self.separatorLayer2]; - - self.separatorLayer3 = [QDCommonUI generateSeparatorLayer]; - [self.view.layer addSublayer:self.separatorLayer3]; -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - CGFloat contentMinY = self.qmui_navigationBarMaxYInViewCoordinator; - CGFloat buttonSpacingHeight = QDButtonSpacingHeight; - CGSize buttonSize = CGSizeMake(260, 40); - CGFloat buttonMinX = CGFloatGetCenter(CGRectGetWidth(self.view.bounds), buttonSize.width); - CGFloat buttonOffsetY = CGFloatGetCenter(buttonSpacingHeight, buttonSize.height); - - self.fillButton1.frame = CGRectFlatMake(buttonMinX, contentMinY + buttonOffsetY, buttonSize.width, buttonSize.height); - self.fillButton2.frame = CGRectFlatMake(buttonMinX, contentMinY + buttonSpacingHeight + buttonOffsetY, buttonSize.width, buttonSize.height); - self.fillButton3.frame = CGRectFlatMake(buttonMinX, contentMinY + buttonSpacingHeight * 2 + buttonOffsetY, buttonSize.width, buttonSize.height); - - self.separatorLayer1.frame = CGRectMake(0, contentMinY + buttonSpacingHeight - PixelOne, CGRectGetWidth(self.view.bounds), PixelOne); - self.separatorLayer2.frame = CGRectSetY(self.separatorLayer1.frame, contentMinY + buttonSpacingHeight * 2 - PixelOne); - self.separatorLayer3.frame = CGRectSetY(self.separatorLayer1.frame, contentMinY + buttonSpacingHeight * 3 - PixelOne); -} - -- (void)handleFillButtonEvent:(id)sender { - UIColor *color = [QDCommonUI randomThemeColor]; - self.fillButton3.fillColor = color; - self.fillButton3.titleTextColor = UIColorWhite; -} - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDFoldCollectionViewLayout.m b/qmuidemo/Modules/Demos/UIKit/QDFoldCollectionViewLayout.m index 21f8e330..d1f74458 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDFoldCollectionViewLayout.m +++ b/qmuidemo/Modules/Demos/UIKit/QDFoldCollectionViewLayout.m @@ -32,7 +32,7 @@ - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { } - (CGSize)collectionViewContentSize { - return CGSizeMake(self.collectionView.frame.size.width, self.collectionView.frame.size.height - self.collectionView.qmui_contentInset.top); + return CGSizeMake(self.collectionView.frame.size.width, self.collectionView.frame.size.height - self.collectionView.adjustedContentInset.top); } - (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect { diff --git a/qmuidemo/Modules/Demos/UIKit/QDFontViewController.m b/qmuidemo/Modules/Demos/UIKit/QDFontViewController.m index 7b747d7f..09db2661 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDFontViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDFontViewController.m @@ -13,15 +13,22 @@ @implementation QDFontViewController - (void)initDataSource { self.dataSource = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: @"默认", [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: - @"UIFontMake", @"", - @"UIFontItalicMake", @"", - @"UIFontBoldMake", @"", - @"UIFontLightMake", @"", + @"UIFontMake", @"默认字重", + @"UIFontLightMake", @"系统细体", + @"UIFontMediumMake", @"系统加粗", + @"UIFontBoldMake", @"系统加粗更粗", + nil], + @"斜体", [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"Regular-Italic", @"", + @"Light-Italic", @"", + @"Medium-Italic", @"", + @"Bold-Italic", @"", nil], @"动态字体", [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: @"UIDynamicFontMake", @"", - @"UIDynamicFontBoldMake", @"", @"UIDynamicFontLightMake", @"", + @"UIDynamicFontMediumMake", @"", + @"UIDynamicFontBoldMake", @"", nil], nil]; } @@ -47,27 +54,50 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N UIFont *font = nil; CGFloat pointSize = 15; if ([keyName isEqualToString:@"UIFontMake"]) { + // 普通 font = UIFontMake(pointSize); - } else if ([keyName isEqualToString:@"UIFontItalicMake"]) { - font = UIFontItalicMake(pointSize); } else if ([keyName isEqualToString:@"UIFontBoldMake"]) { + // 加粗 font = UIFontBoldMake(pointSize); } else if ([keyName isEqualToString:@"UIFontLightMake"]) { + // 细体 font = UIFontLightMake(pointSize); + } else if ([keyName isEqualToString:@"UIFontMediumMake"]) { + // 中档加粗 + font = UIFontMediumMake(pointSize); + } else if ([keyName isEqualToString:@"Regular-Italic"]) { + // 普通斜体 + font = UIFontItalicMake(pointSize); + } else if ([keyName isEqualToString:@"Bold-Italic"]) { + // 更加粗斜体 + font = [UIFont qmui_systemFontOfSize:pointSize weight:QMUIFontWeightBold italic:YES]; + } else if ([keyName isEqualToString:@"Light-Italic"]) { + // 细斜体 + font = [UIFont qmui_systemFontOfSize:pointSize weight:QMUIFontWeightLight italic:YES]; + } else if ([keyName isEqualToString:@"Medium-Italic"]) { + // 加粗斜体 + font = [UIFont qmui_systemFontOfSize:pointSize weight:QMUIFontWeightMedium italic:YES]; } else if ([keyName isEqualToString:@"UIDynamicFontMake"]) { + // 普通动态字体 font = UIDynamicFontMake(pointSize); } else if ([keyName isEqualToString:@"UIDynamicFontBoldMake"]) { + // 更加粗动态字体 font = UIDynamicFontBoldMake(pointSize); } else if ([keyName isEqualToString:@"UIDynamicFontLightMake"]) { + // 细动态字体 font = UIDynamicFontLightMake(pointSize); + } else if ([keyName isEqualToString:@"UIDynamicFontMediumMake"]) { + // 加粗动态字体 + font = UIDynamicFontMediumMake(pointSize); } cell.textLabel.font = font; + cell.detailTextLabel.font = font; return cell; } - (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { - if (section == 1) { - NSString *path = IS_SIMULATOR ? @"设置-通用-辅助功能-Larger Text" : @"设置-显示与亮度-文字大小"; + if (section == 2) { + NSString *path = IS_SIMULATOR ? @"模拟器-设置-辅助功能-显示与文字大小-更大字体" : @"设置-显示与亮度-文字大小"; return [NSString stringWithFormat:@"请到“%@”里修改文字大小再观察当前界面的变化", path]; } return nil; diff --git a/qmuidemo/Modules/Demos/UIKit/QDGhostButtonViewController.h b/qmuidemo/Modules/Demos/UIKit/QDGhostButtonViewController.h deleted file mode 100644 index 69d0d2b7..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDGhostButtonViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDGhostButtonViewController.h -// qmuidemo -// -// Created by QMUI Team on 15/5/23. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QDCommonViewController.h" - -@interface QDGhostButtonViewController : QDCommonViewController - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDGhostButtonViewController.m b/qmuidemo/Modules/Demos/UIKit/QDGhostButtonViewController.m deleted file mode 100644 index b19d13a0..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDGhostButtonViewController.m +++ /dev/null @@ -1,77 +0,0 @@ -// -// QDGhostButtonViewController.m -// qmuidemo -// -// Created by QMUI Team on 15/5/23. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QDGhostButtonViewController.h" -#import "QDCommonUI.h" - -@interface QDGhostButtonViewController () - -@property(nonatomic, strong) QMUIGhostButton *ghostButton1; -@property(nonatomic, strong) QMUIGhostButton *ghostButton2; -@property(nonatomic, strong) QMUIGhostButton *ghostButton3; -@property(nonatomic, strong) CALayer *separatorLayer1; -@property(nonatomic, strong) CALayer *separatorLayer2; -@property(nonatomic, strong) CALayer *separatorLayer3; -@end - -@implementation QDGhostButtonViewController - -- (void)initSubviews { - [super initSubviews]; - self.ghostButton1 = [[QMUIGhostButton alloc] initWithGhostType:QMUIGhostButtonColorBlue]; - self.ghostButton1.titleLabel.font = UIFontMake(14); - [self.ghostButton1 setTitle:@"QMUIGhostButtonColorBlue" forState:UIControlStateNormal]; - [self.view addSubview:self.ghostButton1]; - - self.ghostButton2 = [[QMUIGhostButton alloc] initWithGhostType:QMUIGhostButtonColorRed]; - self.ghostButton2.titleLabel.font = UIFontMake(14); - [self.ghostButton2 setTitle:@"QMUIGhostButtonColorRed" forState:UIControlStateNormal]; - [self.view addSubview:self.ghostButton2]; - - self.ghostButton3 = [[QMUIGhostButton alloc] initWithGhostType:QMUIGhostButtonColorGreen]; - self.ghostButton3.titleLabel.font = UIFontMake(14); - [self.ghostButton3 setTitle:@"点击修改ghostColor" forState:UIControlStateNormal]; - [self.ghostButton3 setImage:UIImageMake(@"icon_emotion") forState:UIControlStateNormal]; - self.ghostButton3.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 6); - self.ghostButton3.adjustsImageWithGhostColor = YES; - [self.ghostButton3 addTarget:self action:@selector(handleGhostButtonColorEvent) forControlEvents:UIControlEventTouchUpInside]; - [self.view addSubview:self.ghostButton3]; - - self.separatorLayer1 = [QDCommonUI generateSeparatorLayer]; - [self.view.layer addSublayer:self.separatorLayer1]; - - self.separatorLayer2 = [QDCommonUI generateSeparatorLayer]; - [self.view.layer addSublayer:self.separatorLayer2]; - - self.separatorLayer3 = [QDCommonUI generateSeparatorLayer]; - [self.view.layer addSublayer:self.separatorLayer3]; -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - CGFloat contentMinY = self.qmui_navigationBarMaxYInViewCoordinator; - CGFloat buttonSpacingHeight = QDButtonSpacingHeight; - CGSize buttonSize = CGSizeMake(260, 40); - CGFloat buttonMinX = CGFloatGetCenter(CGRectGetWidth(self.view.bounds), buttonSize.width); - CGFloat buttonOffsetY = CGFloatGetCenter(buttonSpacingHeight, buttonSize.height); - - self.ghostButton1.frame = CGRectFlatMake(buttonMinX, contentMinY + buttonOffsetY, buttonSize.width, buttonSize.height); - self.ghostButton2.frame = CGRectFlatMake(buttonMinX, contentMinY + buttonSpacingHeight + buttonOffsetY, buttonSize.width, buttonSize.height); - self.ghostButton3.frame = CGRectFlatMake(buttonMinX, contentMinY + buttonSpacingHeight * 2 + buttonOffsetY, buttonSize.width, buttonSize.height); - - self.separatorLayer1.frame = CGRectMake(0, contentMinY + buttonSpacingHeight - PixelOne, CGRectGetWidth(self.view.bounds), PixelOne); - self.separatorLayer2.frame = CGRectSetY(self.separatorLayer1.frame, contentMinY + buttonSpacingHeight * 2 - PixelOne); - self.separatorLayer3.frame = CGRectSetY(self.separatorLayer1.frame, contentMinY + buttonSpacingHeight * 3 - PixelOne); -} - -- (void)handleGhostButtonColorEvent { - UIColor *color = [QDCommonUI randomThemeColor]; - self.ghostButton3.ghostColor = color; -} - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDImageViewController.m b/qmuidemo/Modules/Demos/UIKit/QDImageViewController.m index 386b6c35..f0a562a9 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDImageViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDImageViewController.m @@ -38,6 +38,7 @@ - (void)initDataSource { @"+ qmui_imageWithColor:", @"生成一张纯色的矩形图片,默认大小为(4, 4)", @"+ qmui_imageWithColor:size:cornerRadius:", @"生成一张纯色的矩形图片,可指定图片的大小和圆角", @"+ qmui_imageWithColor:size:cornerRadiusArray:", @"同上,但四个角的圆角值允许不相等", + @"+ qmui_imageWithGradientColors:type:locations:size:cornerRadiusArray:", @"生成渐变图片", @"+ qmui_imageWithStrokeColor:size:path:addClip:", @"将一条路径绘制到指定大小的画图里,并返回生成的图片", @"+ qmui_imageWithStrokeColor:size:lineWidth:cornerRadius:", @"生成一张指定大小的矩形图片,背景透明,带描边和圆角", @"+ qmui_imageWithStrokeColor:size:lineWidth:borderPosition:", @"生成一张指定大小的矩形图片,允许在各个方向选择添加边框", @@ -53,7 +54,7 @@ - (void)initSubviews { [super initSubviews]; CGFloat maximumContentViewWidth = fmin(CGRectGetWidth(self.view.bounds), [QMUIHelper screenSizeFor47Inch].width) - 20 * 2; self.contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, maximumContentViewWidth, 0)]; - self.contentView.backgroundColor = UIColorWhite; + self.contentView.backgroundColor = UIColor.qd_backgroundColorLighten; self.contentView.layer.cornerRadius = 6; self.scrollView = [[UIScrollView alloc] initWithFrame:self.contentView.bounds]; @@ -61,12 +62,10 @@ - (void)initSubviews { self.scrollView.alwaysBounceHorizontal = NO; self.scrollView.alwaysBounceVertical = NO; self.scrollView.scrollsToTop = NO; - if (@available(iOS 11, *)) { - self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } + self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; [self.contentView addSubview:self.scrollView]; - self.methodNameLabel = [[UILabel alloc] qmui_initWithFont:CodeFontMake(16) textColor:[QDThemeManager sharedInstance].currentTheme.themeCodeColor]; + self.methodNameLabel = [[UILabel alloc] qmui_initWithFont:CodeFontMake(16) textColor:UIColor.qd_codeColor]; self.methodNameLabel.numberOfLines = 0; self.methodNameLabel.lineBreakMode = NSLineBreakByCharWrapping; [self.scrollView addSubview:self.methodNameLabel]; @@ -80,7 +79,7 @@ - (void)viewDidLayoutSubviews { - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { QMUITableViewCell *cell = (QMUITableViewCell *)[super tableView:tableView cellForRowAtIndexPath:indexPath]; cell.textLabel.font = CodeFontMake(14); - cell.textLabel.textColor = [QDThemeManager sharedInstance].currentTheme.themeCodeColor; + cell.textLabel.textColor = UIColor.qd_codeColor; cell.detailTextLabel.font = UIFontMake(12); cell.detailTextLabel.textColor = UIColorGray; cell.detailTextLabelEdgeInsets = UIEdgeInsetsMake(2, 0, 0, 0); @@ -142,6 +141,8 @@ - (void)didSelectCellWithTitle:(NSString *)title { contentSizeHeight = [self generateExampleViewForImageWithAttributedString]; } else if ([title isEqualToString:@"+ qmui_imageWithView:"] || [title isEqualToString:@"+ qmui_imageWithView:afterScreenUpdates:"]) { contentSizeHeight = [self generateExampleViewForImageWithView]; + } else if ([title isEqualToString:@"+ qmui_imageWithGradientColors:type:locations:size:cornerRadiusArray:"]) { + contentSizeHeight = [self generateExampleViewForImageWithGradientColors]; } self.scrollView.contentSize = CGSizeMake(contentViewLimitWidth, contentSizeHeight); @@ -179,7 +180,7 @@ - (CGFloat)generateExampleViewForAverageColor { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -214,7 +215,7 @@ - (CGFloat)generateExampleViewForGrayImage { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -249,7 +250,7 @@ - (CGFloat)generateExampleViewForImageWithAlpha { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -293,7 +294,7 @@ - (CGFloat)generateExampleViewForImageWithAlpha { - (CGFloat)generateExampleViewForImageWithTintColor { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -328,7 +329,7 @@ - (CGFloat)generateExampleViewForImageWithBlendColor { CGFloat minY = [self contentViewLayoutStartingMinY]; CGFloat contentWidth = [self contentViewLimitWidth]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -362,14 +363,14 @@ - (CGFloat)generateExampleViewForImageWithBlendColor { - (CGFloat)generateExampleViewForImageWithImageAbove { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; - UIImageView *originImageView = [[UIImageView alloc] initWithImage:[UIImageMake(@"icon_emotion") qmui_imageWithTintColor:[QDThemeManager sharedInstance].currentTheme.themeTintColor]]; + UIImageView *originImageView = [[UIImageView alloc] initWithImage:[UIImageMake(@"icon_emotion") qmui_imageWithTintColor:UIColor.qd_tintColor]]; originImageView.contentMode = UIViewContentModeScaleAspectFit; originImageView.frame = CGRectSetY(originImageView.frame, minY); [self.scrollView addSubview:originImageView]; @@ -397,7 +398,7 @@ - (CGFloat)generateExampleViewForImageWithImageAbove { - (CGFloat)generateExampleViewForImageWithSpacingExtensionInsets { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图(UIImageView带边框)"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -434,7 +435,7 @@ - (CGFloat)generateExampleViewForImageWithClippedCornerRadius { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -474,7 +475,7 @@ - (CGFloat)generateExampleViewForImageWithClippedRect { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -511,7 +512,7 @@ - (CGFloat)generateExampleViewForResizedImage { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -549,7 +550,7 @@ - (CGFloat)generateExampleViewForResizedImage { - (CGFloat)generateExampleViewForImageWithDirection { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -589,7 +590,7 @@ - (CGFloat)generateExampleViewForImageWithDirection { - (CGFloat)generateExampleViewForImageWithBorder { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -625,7 +626,7 @@ - (CGFloat)generateExampleViewForImageWithBorder { - (CGFloat)generateExampleViewForImageWithBorderColorAndCornerRadiusWithDashedBorder:(BOOL)dashedBorder { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -664,7 +665,7 @@ - (CGFloat)generateExampleViewForImageWithBorderColorAndCornerRadiusWithDashedBo - (CGFloat)generateExampleViewForImageWithBorderColorAndCornerRadiusAndPosition { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -698,7 +699,7 @@ - (CGFloat)generateExampleViewForImageWithMaskImage { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"处理前的原图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -713,7 +714,7 @@ - (CGFloat)generateExampleViewForImageWithMaskImage { [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; - UILabel *maskImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *maskImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; maskImageLabel.text = @"A.用来做遮罩的图片"; [maskImageLabel sizeToFit]; maskImageLabel.frame = CGRectSetY(maskImageLabel.frame, minY); @@ -742,7 +743,7 @@ - (CGFloat)generateExampleViewForImageWithMaskImage { [self.scrollView addSubview:afterImageView]; minY = CGRectGetMaxY(afterImageView.frame) + 16; - UILabel *maskImageLabel2 = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *maskImageLabel2 = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; maskImageLabel2.text = @"B.用来做遮罩的图片"; [maskImageLabel2 sizeToFit]; maskImageLabel2.frame = CGRectSetY(maskImageLabel2.frame, minY); @@ -779,7 +780,7 @@ - (CGFloat)generateExampleViewForImageWithColor { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"生成一张圆角图片"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -799,7 +800,7 @@ - (CGFloat)generateExampleViewForImageWithColorAndCornerRadiusArray { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"生成一张图片,右边带圆角"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -815,11 +816,30 @@ - (CGFloat)generateExampleViewForImageWithColorAndCornerRadiusArray { return minY; } +- (CGFloat)generateExampleViewForImageWithGradientColors { + CGFloat minY = [self contentViewLayoutStartingMinY]; + + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; + originImageLabel.text = @"生成一张渐变图片,支持多种渐变方向"; + [originImageLabel sizeToFit]; + originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); + [self.scrollView addSubview:originImageLabel]; + minY = CGRectGetMaxY(originImageLabel.frame) + 6; + + UIImage *originImage = [UIImage qmui_imageWithGradientColors:@[[QDCommonUI randomThemeColor], [QDCommonUI randomThemeColor]] type:QMUIImageGradientTypeHorizontal locations:nil size:CGSizeMake(100, 40) cornerRadiusArray:nil]; + UIImageView *originImageView = [[UIImageView alloc] initWithImage:originImage]; + originImageView.frame = CGRectSetY(originImageView.frame, minY); + [self.scrollView addSubview:originImageView]; + minY = CGRectGetMaxY(originImageView.frame) + 16; + + return minY; +} + - (CGFloat)generateExampleViewForImageWithStrokeColorAndPath { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"用椭圆路径生成一张图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -842,7 +862,7 @@ - (CGFloat)generateExampleViewForImageWithStrokeColorAndCornerRadius { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"在给定的大小里绘制一条带圆角的路径"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -862,7 +882,7 @@ - (CGFloat)generateExampleViewForImageWithStrokeColorAndBorderPosition { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"在左、下、右绘制一条边框"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -882,14 +902,14 @@ - (CGFloat)generateExampleViewForImageWithShape { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *titleLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *titleLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; titleLabel.text = @"生成预设的形状图片"; [titleLabel sizeToFit]; titleLabel.frame = CGRectSetY(titleLabel.frame, minY); [self.scrollView addSubview:titleLabel]; minY = CGRectGetMaxY(titleLabel.frame) + 6; - UIColor *tintColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + UIColor *tintColor = UIColor.qd_tintColor; UIImage *ovalImage = [UIImage qmui_imageWithShape:QMUIImageShapeOval size:CGSizeMake(contentWidth / 4, 20) tintColor:tintColor]; UIImage *triangleImage = [UIImage qmui_imageWithShape:QMUIImageShapeTriangle size:CGSizeMake(6, 6) tintColor:tintColor]; UIImage *disclosureIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeDisclosureIndicator size:CGSizeMake(8, 13) tintColor:tintColor]; @@ -910,7 +930,7 @@ - (CGFloat)generateExampleViewForImageWithShape { } - (CGFloat)generateExampleLabelAndImageViewWithImage:(UIImage *)image shapeName:(NSString *)shapeName minY:(CGFloat)minY { - UILabel *exampleLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(12) textColor:UIColorGrayDarken]; + UILabel *exampleLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(12) textColor:UIColor.qd_mainTextColor]; exampleLabel.text = shapeName; [exampleLabel sizeToFit]; exampleLabel.frame = CGRectSetY(exampleLabel.frame, minY); @@ -927,7 +947,7 @@ - (CGFloat)generateExampleLabelAndImageViewWithImage:(UIImage *)image shapeName: - (CGFloat)generateExampleViewForImageWithAttributedString { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"将NSAttributedString生成为一张图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -959,7 +979,7 @@ - (CGFloat)generateExampleViewForImageWithView { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *originImageLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; originImageLabel.text = @"将当前UINavigationController.view截图"; [originImageLabel sizeToFit]; originImageLabel.frame = CGRectSetY(originImageLabel.frame, minY); @@ -970,7 +990,7 @@ - (CGFloat)generateExampleViewForImageWithView { UIImageView *originImageView = [[UIImageView alloc] initWithImage:originImage]; originImageView.contentMode = UIViewContentModeScaleAspectFit; originImageView.layer.borderWidth = PixelOne; - originImageView.layer.borderColor = UIColorGrayLighten.CGColor; + originImageView.layer.borderColor = UIColor.qd_separatorColor.CGColor; [originImageView qmui_sizeToFitKeepingImageAspectRatioInSize:CGSizeMake(contentWidth, CGFLOAT_MAX)]; originImageView.frame = CGRectSetY(originImageView.frame, minY); [self.scrollView addSubview:originImageView]; diff --git a/qmuidemo/Modules/Demos/UIKit/QDImageViewViewController.m b/qmuidemo/Modules/Demos/UIKit/QDImageViewViewController.m index 91c845be..55e683f2 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDImageViewViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDImageViewViewController.m @@ -8,6 +8,10 @@ #import "QDImageViewViewController.h" +@interface QDImageViewTableViewCell : UITableViewCell + +@end + @interface QDImageViewViewController () @property(nonatomic, assign) BOOL usingSmoothAnimation; @@ -46,22 +50,22 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier]; cell.selectionStyle = UITableViewCellSelectionStyleNone; - cell.textLabel.attributedText = [[NSAttributedString alloc] initWithString:@"qmui_smoothAnimation" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColorGray1, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:22]}]; - cell.detailTextLabel.attributedText = [[NSAttributedString alloc] initWithString:@"UIImageView (QMUI) 默认打开了 qmui_smoothAnimation,以支持在 UIScrollView 内使用 animatedImage 时依然能保证界面的流畅(系统在这种情况下会有明显的卡顿)。可通过切换右边的开关来对比效果。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColorGray6, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:16]}]; + cell.backgroundColor = TableViewCellBackgroundColor; + cell.textLabel.attributedText = [[NSAttributedString alloc] initWithString:@"qmui_smoothAnimation" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColor.qd_mainTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:22]}]; + cell.detailTextLabel.attributedText = [[NSAttributedString alloc] initWithString:@"UIImageView (QMUI) 默认打开了 qmui_smoothAnimation,以支持在 UIScrollView 内使用 animatedImage 时依然能保证界面的流畅(系统在这种情况下会有明显的卡顿)。可通过切换右边的开关来对比效果。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:16]}]; cell.detailTextLabel.numberOfLines = 0; UISwitch *switchControl = [[UISwitch alloc] init]; [switchControl sizeToFit]; switchControl.transform = CGAffineTransformMakeScale(.8, .8); [switchControl addTarget:self action:@selector(handleSwitchEvent:) forControlEvents:UIControlEventTouchUpInside]; - switchControl.onTintColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; - switchControl.tintColor = switchControl.onTintColor; cell.accessoryView = switchControl; } ((UISwitch *)cell.accessoryView).on = self.usingSmoothAnimation; } else if ([identifier isEqualToString:@"image"]) { if (!cell) { - cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; + cell = [[QDImageViewTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; cell.selectionStyle = UITableViewCellSelectionStyleNone; + cell.backgroundColor = TableViewCellBackgroundColor; cell.imageView.image = self.animatedImage; } cell.imageView.qmui_smoothAnimation = self.usingSmoothAnimation; @@ -79,3 +83,17 @@ - (void)handleSwitchEvent:(UISwitch *)switchControl { } @end + +@implementation QDImageViewTableViewCell + +// iOS 13 下系统无法正确地给 imageView 布局,size 总是为 0,所以这里写一个 subclass 专门布局 imageView +// iOS 12 及以下用系统的 UITableViewCell 就行了没啥问题 +- (void)layoutSubviews { + [super layoutSubviews]; + CGPoint imageViewOrigin = self.imageView.frame.origin; + CGSize imageViewLimitSize = CGSizeMake(self.contentView.bounds.size.width - imageViewOrigin.x * 2, self.contentView.bounds.size.width - imageViewOrigin.y * 2); + [self.imageView qmui_sizeToFitKeepingImageAspectRatioInSize:imageViewLimitSize]; + self.imageView.qmui_top = self.imageView.qmui_topWhenCenterInSuperview; +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDInsetGroupedTableViewController.h b/qmuidemo/Modules/Demos/UIKit/QDInsetGroupedTableViewController.h new file mode 100644 index 00000000..85026fc8 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDInsetGroupedTableViewController.h @@ -0,0 +1,17 @@ +// +// QDInsetGroupedTableViewController.h +// qmuidemo +// +// Created by MoLice on 2020/6/1. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDCommonTableViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDInsetGroupedTableViewController : QDCommonTableViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/UIKit/QDInsetGroupedTableViewController.m b/qmuidemo/Modules/Demos/UIKit/QDInsetGroupedTableViewController.m new file mode 100644 index 00000000..e4428ae9 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDInsetGroupedTableViewController.m @@ -0,0 +1,118 @@ +// +// QDInsetGroupedTableViewController.m +// qmuidemo +// +// Created by MoLice on 2020/6/1. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDInsetGroupedTableViewController.h" +#import "QDDynamicHeightTableViewCell.h" + +@interface QDInsetGroupedTableViewController () + +@property(nonatomic, strong) NSArray *texts; +@end + +@implementation QDInsetGroupedTableViewController + +- (instancetype)init { + // 只要传进去 style 即可使用,其他东西与普通列表用法一致 + return [self initWithStyle:UITableViewStyleInsetGrouped]; +} + +- (void)didInitializeWithStyle:(UITableViewStyle)style { + [super didInitializeWithStyle:style]; + self.texts = @[ + @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。", + @"UIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。", + @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。\nUIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。\n高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。" + ]; +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + self.navigationItem.rightBarButtonItem = [UIBarButtonItem qmui_itemWithImage:UIImageMake(@"icon_nav_about") target:self action:@selector(handleDebugItemEvent)]; +} + +- (void)handleDebugItemEvent { + __weak __typeof(self)weakSelf = self; + QMUIPopupMenuView *menu = [[QMUIPopupMenuView alloc] init]; + menu.items = @[ + [QMUIPopupMenuItem itemWithTitle:@"标准间距" handler:^(__kindof QMUIPopupMenuItem * _Nonnull aItem, __kindof UIControl * _Nonnull aItemView, NSInteger section, NSInteger index) { + if ([aItem.title isEqualToString:@"标准间距"]) { + aItem.title = @"紧凑间距"; + weakSelf.tableView.qmui_insetGroupedHorizontalInset = 10; + } else { + aItem.title = @"标准间距"; + weakSelf.tableView.qmui_insetGroupedHorizontalInset = TableViewInsetGroupedHorizontalInset; + } + }], + [QMUIPopupMenuItem itemWithTitle:@"标准圆角" handler:^(__kindof QMUIPopupMenuItem * _Nonnull aItem, __kindof UIControl * _Nonnull aItemView, NSInteger section, NSInteger index) { + if ([aItem.title isEqualToString:@"标准圆角"]) { + aItem.title = @"小圆角"; + weakSelf.tableView.qmui_insetGroupedCornerRadius = 3; + } else { + aItem.title = @"标准圆角"; + weakSelf.tableView.qmui_insetGroupedCornerRadius = TableViewInsetGroupedCornerRadius; + } + }], + [QMUIPopupMenuItem itemWithTitle:@"进入编辑" handler:^(__kindof QMUIPopupMenuItem * _Nonnull aItem, __kindof UIControl * _Nonnull aItemView, NSInteger section, NSInteger index) { + if ([aItem.title isEqualToString:@"进入编辑"]) { + aItem.title = @"退出编辑"; + weakSelf.tableView.editing = YES; + } else { + aItem.title = @"进入编辑"; + weakSelf.tableView.editing = NO; + } + [weakSelf.tableView reloadData]; + + }], + ]; + menu.automaticallyHidesWhenUserTap = YES; + menu.maskViewBackgroundColor = nil; + menu.sourceBarItem = self.navigationItem.rightBarButtonItem; + [menu showWithAnimated:YES]; +} + +#pragma mark - + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 3; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return 3; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *identifier = @"cell"; + QDDynamicHeightTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; + if (!cell) { + cell = [[QDDynamicHeightTableViewCell alloc] initForTableView:tableView withReuseIdentifier:identifier]; + } + [cell renderWithNameText:[NSString stringWithFormat:@"%@ - %@", @(indexPath.section), @(indexPath.row)] contentText:self.texts[indexPath.row]]; + return cell; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [tableView deselectRowAtIndexPath:indexPath animated:YES]; +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { + return [NSString stringWithFormat:@"Section Header %@", @(section)]; +} + +- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { + return [NSString stringWithFormat:@"Section Footer %@", @(section)]; +} + +- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { + return YES; +} + +- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { + +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDInterceptBackButtonEventViewController.m b/qmuidemo/Modules/Demos/UIKit/QDInterceptBackButtonEventViewController.m index 9752529f..0cf74930 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDInterceptBackButtonEventViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDInterceptBackButtonEventViewController.m @@ -19,18 +19,11 @@ @implementation QDInterceptBackButtonEventViewController #pragma mark - Lift Circle -- (instancetype)init { - self = [super init]; - if (self) { - self.automaticallyAdjustsScrollViewInsets = NO; - } - return self; -} - - (void)initSubviews { [super initSubviews]; _textView = [[QMUITextView alloc] init]; + self.textView.backgroundColor = UIColor.qd_backgroundColorLighten; self.textView.placeholder = @"请输入个人简介..."; self.textView.font = UIFontMake(15); self.textView.layer.borderWidth = PixelOne; @@ -38,6 +31,7 @@ - (void)initSubviews { self.textView.layer.cornerRadius = 4; self.textView.textContainerInset = UIEdgeInsetsMake(8, 6, 8, 6); self.textView.delegate = self; + self.textView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; [self.view addSubview:self.textView]; _textCountLabel = [[UILabel alloc] init]; @@ -77,11 +71,7 @@ - (void)setLocalText:(NSString *)text { #pragma mark - UINavigationControllerBackButtonHandlerProtocol -- (BOOL)shouldHoldBackButtonEvent { - return YES; -} - -- (BOOL)canPopViewController { +- (BOOL)shouldPopViewControllerByBackButtonOrPopGesture:(BOOL)byPopGesture { // 这里不要做一些费时的操作,否则可能会卡顿。 if (self.textView.text.length > 0) { [self.textView resignFirstResponder]; diff --git a/qmuidemo/Modules/Demos/UIKit/QDLabelViewController.m b/qmuidemo/Modules/Demos/UIKit/QDLabelViewController.m index 57c8a617..78c45e55 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDLabelViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDLabelViewController.m @@ -8,6 +8,15 @@ #import "QDLabelViewController.h" +@interface QDLabelTruncatingTailView : UIControl + +@property(nonatomic, strong) UIColor *gradientColor; +@property(nonatomic, assign) CGFloat gradientWidth; + +@property(nonatomic, strong) CAGradientLayer *gradientMaskLayer; +@property(nonatomic, strong) UILabel *label; +@end + @interface QDLabelViewController () @property(nonatomic, strong) QMUILabel *label1; @@ -30,7 +39,7 @@ - (void)initSubviews { _label1 = [[QMUILabel alloc] init]; self.label1.text = @"可长按复制"; self.label1.font = UIFontMake(15); - self.label1.textColor = UIColorGray5; + self.label1.textColor = UIColor.qd_descriptionTextColor; self.label1.canPerformCopyAction = YES; self.label1.didCopyBlock = ^(QMUILabel *label, NSString *stringCopied) { [QMUITips showSucceed:@"已复制"]; @@ -42,15 +51,28 @@ - (void)initSubviews { self.label2.text = @"可设置 contentInsets"; self.label2.font = UIFontMake(15); self.label2.textColor = UIColorWhite; - self.label2.backgroundColor = UIColorGray8; + self.label2.backgroundColor = [UIColor qmui_colorWithThemeProvider:^UIColor * _Nonnull(__kindof QMUIThemeManager * _Nonnull manager, __kindof NSObject * _Nullable identifier, NSObject * _Nullable theme) { + return [theme.themeTintColor colorWithAlphaComponent:.5]; + }]; self.label2.contentEdgeInsets = UIEdgeInsetsMake(8, 16, 8, 16); [self.label2 sizeToFit]; [self.view addSubview:self.label2]; _label3 = [[QMUILabel alloc] init]; - self.label3.text = @"复制上面第二个label的样式"; - [self.label3 qmui_setTheSameAppearanceAsLabel:self.label2]; - [self.label3 sizeToFit]; + self.label3.text = @"可支持文字缩略时显示自定义的 View(通常用于展开更多)。例如这个文字特别长,然后最多只显示3行,点击“更多”可以展开完整的文字。例如这个文字特别长,然后最多只显示3行,点击“更多”可以展开完整的文字。例如这个文字特别长,然后最多只显示3行,点击“更多”可以展开完整的文字。例如这个文字特别长,然后最多只显示3行,点击“更多”可以展开完整的文字。例如这个文字特别长,然后最多只显示3行,点击“更多”可以展开完整的文字。"; + self.label3.numberOfLines = 3; + self.label3.font = UIFontMake(15); + self.label3.qmui_lineHeight = 21; + self.label3.textColor = UIColor.qd_descriptionTextColor; + self.label3.userInteractionEnabled = YES;// UILabel 系统默认是不支持点击的,如果你的 truncatingTailView 支持点击,则需要手动开启 userInteractionEnabled + self.label3.truncatingTailView = ({ + QDLabelTruncatingTailView *view = QDLabelTruncatingTailView.new; + view.gradientColor = self.view.backgroundColor; + view.gradientWidth = 48; + view.label.text = @"更多"; + [view addTarget:self action:@selector(handleTruncatingTailViewEvent:) forControlEvents:UIControlEventTouchUpInside]; + view; + }); [self.view addSubview:self.label3]; self.separatorLayer1 = [QDCommonUI generateSeparatorLayer]; @@ -64,6 +86,11 @@ - (void)initSubviews { } +- (void)handleTruncatingTailViewEvent:(QDLabelTruncatingTailView *)view { + self.label3.numberOfLines = 0; + [self.view setNeedsLayout]; +} + - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; CGFloat contentMinY = self.qmui_navigationBarMaxYInViewCoordinator; @@ -77,9 +104,54 @@ - (void)viewDidLayoutSubviews { self.separatorLayer2.frame = CGRectMake(0, contentMinY + buttonSpacingHeight * 2, CGRectGetWidth(self.view.bounds), PixelOne); - self.label3.frame = CGRectSetXY(self.label3.frame, CGFloatGetCenter(CGRectGetWidth(self.view.bounds), CGRectGetWidth(self.label3.bounds)), CGRectGetMaxY(self.separatorLayer2.frame) + CGFloatGetCenter(buttonSpacingHeight, CGRectGetHeight(self.label3.bounds))); + self.label3.frame = CGRectMake(32, CGRectGetMaxY(self.separatorLayer2.frame) + 32, CGRectGetWidth(self.view.bounds) - 32 * 2, QMUIViewSelfSizingHeight); - self.separatorLayer3.frame = CGRectMake(0, contentMinY + buttonSpacingHeight * 3, CGRectGetWidth(self.view.bounds), PixelOne); + self.separatorLayer3.frame = CGRectMake(0, CGRectGetMaxY(self.label3.frame) + 32, CGRectGetWidth(self.view.bounds), PixelOne); +} + +@end + +@implementation QDLabelTruncatingTailView + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.gradientMaskLayer = CAGradientLayer.layer; + [self.gradientMaskLayer qmui_removeDefaultAnimations]; + self.gradientMaskLayer.startPoint = CGPointMake(0, .5); + self.gradientMaskLayer.endPoint = CGPointMake(1, .5); + [self.layer addSublayer:self.gradientMaskLayer]; + + self.label = UILabel.new; + [self addSubview:self.label]; + } + return self; +} + +- (void)didMoveToSuperview { + [super didMoveToSuperview]; + if ([self.superview isKindOfClass:UILabel.class]) { + UILabel *label = (UILabel *)self.superview; + [self.label qmui_setTheSameAppearanceAsLabel:label]; + self.label.backgroundColor = self.gradientColor; + [self setNeedsLayout]; + } +} + +- (void)setGradientColor:(UIColor *)gradientColor { + _gradientColor = gradientColor; + self.gradientMaskLayer.colors = @[(id)[gradientColor colorWithAlphaComponent:0].CGColor, (id)gradientColor.CGColor]; + self.label.backgroundColor = gradientColor; +} + +- (CGSize)sizeThatFits:(CGSize)size { + CGSize labelSize = [self.label sizeThatFits:CGSizeMax]; + return CGSizeMake(self.gradientWidth + labelSize.width, labelSize.height); +} + +- (void)layoutSubviews { + [super layoutSubviews]; + self.gradientMaskLayer.frame = CGRectMake(0, 0, self.gradientWidth, CGRectGetHeight(self.bounds)); + self.label.frame = CGRectMake(CGRectGetMaxX(self.gradientMaskLayer.frame), 0, CGRectGetWidth(self.bounds) - CGRectGetMaxX(self.gradientMaskLayer.frame), CGRectGetHeight(self.bounds)); } @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDLargeTitlesViewController.h b/qmuidemo/Modules/Demos/UIKit/QDLargeTitlesViewController.h new file mode 100644 index 00000000..35a47679 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDLargeTitlesViewController.h @@ -0,0 +1,17 @@ +// +// QDLargeTitlesViewController.h +// qmuidemo +// +// Created by ziezheng on 2019/7/11. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import "QDCommonListViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDLargeTitlesViewController : QDCommonListViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/UIKit/QDLargeTitlesViewController.m b/qmuidemo/Modules/Demos/UIKit/QDLargeTitlesViewController.m new file mode 100644 index 00000000..6ae582a3 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDLargeTitlesViewController.m @@ -0,0 +1,73 @@ +// +// QDLargeTitlesViewController.m +// qmuidemo +// +// Created by ziezheng on 2019/7/11. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import "QDLargeTitlesViewController.h" + +@interface QDLargeTitlesViewController () + +@end + +@implementation QDLargeTitlesViewController + +- (void)initSubviews { + [super initSubviews]; + self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, APPLICATION_HEIGHT)]; +} + +- (void)initDataSource { + [super initDataSource]; + self.dataSourceWithDetailText = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"push 一个不显示大标题的 vc", @"LargeTitleDisplayModeNever", + @"push 一个显示大标题的 vc", @"LargeTitleDisplayModeAlways", + @"push 一个跟随上页设置的 vc", @"LargeTitleDisplayModeAutomatic", + @"滚动试试", @"这是一个可以滚动的页面", + nil]; +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + self.title = @"LargeTitle"; + self.navigationController.navigationBar.prefersLargeTitles = YES; +} + +- (void)didSelectCellWithTitle:(NSString *)title { + if ([title isEqualToString:@"滚动试试"]) { + CGPoint contentOffsetWhenLargeTitleDisplaying = CGPointMake(0, -(NavigationContentTop + 52)); + if (CGPointEqualToPoint(self.tableView.contentOffset, contentOffsetWhenLargeTitleDisplaying)) { + [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:3 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES]; + } else { + [self.tableView setContentOffset:contentOffsetWhenLargeTitleDisplaying animated:YES]; + } + [self.tableView qmui_clearsSelection]; + } else { + QDLargeTitlesViewController *largeTitlesViewController = [[QDLargeTitlesViewController alloc] init]; + + UINavigationItemLargeTitleDisplayMode displayMode; + if ([title isEqualToString:@"push 一个不显示大标题的 vc"]) { + displayMode = UINavigationItemLargeTitleDisplayModeNever; + } else if ([title isEqualToString:@"push 一个显示大标题的 vc"]) { + displayMode = UINavigationItemLargeTitleDisplayModeAlways; + } else { + displayMode = UINavigationItemLargeTitleDisplayModeAutomatic; + } + + largeTitlesViewController.navigationItem.largeTitleDisplayMode = displayMode; + [self.navigationController pushViewController:largeTitlesViewController animated:YES]; + } +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + // 上个界面如果不是 QDLargeTitlesViewController 就还原 prefersLargeTitles,以免影响其他界面 + UIViewController *currentViewController = UIApplication.sharedApplication.keyWindow.rootViewController.qmui_visibleViewControllerIfExist; + if ([currentViewController class] != [QDLargeTitlesViewController class]) { + currentViewController.navigationController.navigationBar.prefersLargeTitles = NO; + } +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDLinkButtonViewController.h b/qmuidemo/Modules/Demos/UIKit/QDLinkButtonViewController.h deleted file mode 100644 index 5522e0c4..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDLinkButtonViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDLinkButtonViewController.h -// qmuidemo -// -// Created by QMUI Team on 16/10/12. -// Copyright (c) 2016年 QMUI Team. All rights reserved. -// - -#import "QDCommonViewController.h" - -@interface QDLinkButtonViewController : QDCommonViewController - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDLinkButtonViewController.m b/qmuidemo/Modules/Demos/UIKit/QDLinkButtonViewController.m deleted file mode 100644 index dc17b8e8..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDLinkButtonViewController.m +++ /dev/null @@ -1,73 +0,0 @@ -// -// QDLinkButtonViewController.m -// qmuidemo -// -// Created by QMUI Team on 16/10/12. -// Copyright (c) 2016年 QMUI Team. All rights reserved. -// - -#import "QDLinkButtonViewController.h" -#import "QDCommonUI.h" - -@interface QDLinkButtonViewController () - -@property(nonatomic, strong) QMUILinkButton *linkButton1; -@property(nonatomic, strong) CALayer *separatorLayer1; -@property(nonatomic, strong) QMUILinkButton *linkButton2; -@property(nonatomic, strong) CALayer *separatorLayer2; -@property(nonatomic, strong) QMUILinkButton *linkButton3; -@property(nonatomic, strong) CALayer *separatorLayer3; -@end - -@implementation QDLinkButtonViewController - -- (void)initSubviews { - [super initSubviews]; - - self.linkButton1 = [self generateLinkButtonWithTitle:@"带下划线的按钮"]; - [self.view addSubview:self.linkButton1]; - - self.separatorLayer1 = [QDCommonUI generateSeparatorLayer]; - [self.view.layer addSublayer:self.separatorLayer1]; - - self.linkButton2 = [self generateLinkButtonWithTitle:@"修改下划线颜色"]; - self.linkButton2.underlineColor = UIColorTheme8; - [self.view addSubview:self.linkButton2]; - - self.separatorLayer2 = [QDCommonUI generateSeparatorLayer]; - [self.view.layer addSublayer:self.separatorLayer2]; - - self.linkButton3 = [self generateLinkButtonWithTitle:@"修改下划线的位置"]; - self.linkButton3.underlineInsets = UIEdgeInsetsMake(0, 32, 0, 46); - [self.view addSubview:self.linkButton3]; - - self.separatorLayer3 = [QDCommonUI generateSeparatorLayer]; - [self.view.layer addSublayer:self.separatorLayer3]; - -} - -- (QMUILinkButton *)generateLinkButtonWithTitle:(NSString *)title { - QMUILinkButton *linkButton = [[QMUILinkButton alloc] init]; - linkButton.adjustsTitleTintColorAutomatically = YES; - linkButton.tintColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; - linkButton.titleLabel.font = UIFontMake(15); - [linkButton setTitle:title forState:UIControlStateNormal]; - [linkButton sizeToFit]; - return linkButton; -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - CGFloat contentMinY = self.qmui_navigationBarMaxYInViewCoordinator; - CGFloat buttonSpacingHeight = 64; - - self.separatorLayer1.frame = CGRectMake(0, contentMinY + buttonSpacingHeight - PixelOne, CGRectGetWidth(self.view.bounds), PixelOne); - self.separatorLayer2.frame = CGRectSetY(self.separatorLayer1.frame, contentMinY + buttonSpacingHeight * 2 - PixelOne); - self.separatorLayer3.frame = CGRectSetY(self.separatorLayer1.frame, contentMinY + buttonSpacingHeight * 3 - PixelOne); - - self.linkButton1.frame = CGRectSetXY(self.linkButton1.frame, CGRectGetMinXHorizontallyCenterInParentRect(self.view.bounds, self.linkButton1.frame), contentMinY + CGFloatGetCenter(buttonSpacingHeight, CGRectGetHeight(self.linkButton1.frame))); - self.linkButton2.frame = CGRectSetXY(self.linkButton2.frame, CGRectGetMinXHorizontallyCenterInParentRect(self.view.bounds, self.linkButton2.frame), contentMinY + buttonSpacingHeight + CGFloatGetCenter(buttonSpacingHeight, CGRectGetHeight(self.linkButton2.frame))); - self.linkButton3.frame = CGRectSetXY(self.linkButton3.frame, CGRectGetMinXHorizontallyCenterInParentRect(self.view.bounds, self.linkButton3.frame), contentMinY + buttonSpacingHeight * 2 + CGFloatGetCenter(buttonSpacingHeight, CGRectGetHeight(self.linkButton3.frame))); -} - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDNavigationBarMaxYViewController.m b/qmuidemo/Modules/Demos/UIKit/QDNavigationBarMaxYViewController.m index eca80f93..62f1c320 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDNavigationBarMaxYViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDNavigationBarMaxYViewController.m @@ -30,7 +30,7 @@ - (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger footerView.backgroundView.backgroundColor = UIColorClear; footerView.contentEdgeInsets = UIEdgeInsetsSetTop(footerView.contentEdgeInsets, 0); footerView.titleLabel.font = UIFontMake(12); - footerView.titleLabel.textColor = UIColorGray6; + footerView.titleLabel.textColor = UIColor.qd_descriptionTextColor; footerView.titleLabel.qmui_borderPosition = QMUIViewBorderPositionTop; footerView.titleLabel.qmui_borderColor = TableViewSeparatorColor; return footerView; diff --git a/qmuidemo/Modules/Demos/UIKit/QDNavigationListViewController.m b/qmuidemo/Modules/Demos/UIKit/QDNavigationListViewController.m index 7f8f9500..48e098d3 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDNavigationListViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDNavigationListViewController.m @@ -11,18 +11,45 @@ #import "QDChangeNavBarStyleViewController.h" #import "QDNavigationTransitionViewController.h" #import "QDNavigationBarMaxYViewController.h" +#import "QDLargeTitlesViewController.h" @implementation QDNavigationListViewController +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + if (self.isMovingToParentViewController) { + QMUICMI.needsBackBarButtonItemTitle = YES; + } +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + if (self.isMovingFromParentViewController) { + QMUICMI.needsBackBarButtonItemTitle = NO; + } +} + + - (void)initDataSource { [super initDataSource]; - self.dataSourceWithDetailText = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: - @"拦截系统navBar返回按钮事件", @"例如询问已输入的内容要不要保存", - @"感知系统的手势返回", @"可感知到是否成功手势返回或者中断了", - @"方便控制界面导航栏样式", @"方便控制前后两个界面的导航栏和状态栏样式", - @"优化导航栏在转场时的样式", @"优化系统navController只有一个navBar带来的问题", - @"获取导航栏的正确布局位置", @"特别是前后两个界面导航栏显隐状态不一致时容易出现布局跳动", - nil]; + if ([UINavigationBar instancesRespondToSelector:@selector(prefersLargeTitles)]) { + self.dataSourceWithDetailText = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"拦截系统navBar返回按钮事件", @"例如询问已输入的内容要不要保存", + @"感知系统的手势返回", @"可感知到是否成功手势返回或者中断了", + @"方便控制界面导航栏样式", @"方便控制前后两个界面的导航栏和状态栏样式", + @"优化导航栏在转场时的样式", @"优化系统navController只有一个navBar带来的问题", + @"获取导航栏的正确布局位置", @"特别是前后两个界面导航栏显隐状态不一致时容易出现布局跳动", + @"兼容 LargeTitle", @"感知 LargeTitle 显示与隐藏", + nil]; + } else { + self.dataSourceWithDetailText = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"拦截系统navBar返回按钮事件", @"例如询问已输入的内容要不要保存", + @"感知系统的手势返回", @"可感知到是否成功手势返回或者中断了", + @"方便控制界面导航栏样式", @"方便控制前后两个界面的导航栏和状态栏样式", + @"优化导航栏在转场时的样式", @"优化系统navController只有一个navBar带来的问题", + @"获取导航栏的正确布局位置", @"特别是前后两个界面导航栏显隐状态不一致时容易出现布局跳动", + nil]; + } } - (void)didSelectCellWithTitle:(NSString *)title { @@ -38,12 +65,14 @@ - (void)didSelectCellWithTitle:(NSString *)title { } else if ([title isEqualToString:@"优化导航栏在转场时的样式"]) { viewController = [[QDChangeNavBarStyleViewController alloc] init]; - ((QDChangeNavBarStyleViewController *)viewController).customNavBarTransition = YES; } else if ([title isEqualToString:@"获取导航栏的正确布局位置"]) { viewController = [[QDNavigationBarMaxYViewController alloc] init]; ((QDNavigationBarMaxYViewController *)viewController).navigationBarHidden = YES; } + else if ([title isEqualToString:@"兼容 LargeTitle"]) { + viewController = [[QDLargeTitlesViewController alloc] init]; + } viewController.title = title; [self.navigationController pushViewController:viewController animated:YES]; } diff --git a/qmuidemo/Modules/Demos/UIKit/QDNavigationTransitionViewController.m b/qmuidemo/Modules/Demos/UIKit/QDNavigationTransitionViewController.m index e47d81b7..03a66c06 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDNavigationTransitionViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDNavigationTransitionViewController.m @@ -39,13 +39,13 @@ - (void)resetStateLabel { // 注意,这一切都需要在 QMUINavigationController 里才有效。 #pragma mark - -- (void)navigationController:(QMUINavigationController *)navigationController poppingByInteractiveGestureRecognizer:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer viewControllerWillDisappear:(UIViewController *)viewControllerWillDisappear viewControllerWillAppear:(UIViewController *)viewControllerWillAppear { +- (void)navigationController:(QMUINavigationController *)navigationController poppingByInteractiveGestureRecognizer:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer isCancelled:(BOOL)isCancelled viewControllerWillDisappear:(UIViewController *)viewControllerWillDisappear viewControllerWillAppear:(UIViewController *)viewControllerWillAppear { if (gestureRecognizer.state == UIGestureRecognizerStateEnded) { - if (viewControllerWillDisappear == self) { - [QMUITips showSucceed:@"松手了,界面发生切换"]; - } else if (viewControllerWillAppear == self) { + if (isCancelled) { [QMUITips showInfo:@"松手了,没有触发界面切换"]; + } else { + [QMUITips showSucceed:@"松手了,界面发生切换"]; } [self resetStateLabel]; return; diff --git a/qmuidemo/Modules/Demos/UIKit/QDNormalButtonViewController.m b/qmuidemo/Modules/Demos/UIKit/QDNormalButtonViewController.m index a22981d4..9f2fe8d9 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDNormalButtonViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDNormalButtonViewController.m @@ -26,7 +26,9 @@ - (void)initSubviews { // 普通按钮 self.normalButton = [QDUIHelper generateDarkFilledButton]; + self.normalButton.qmui_height = 50; [self.normalButton setTitle:NSLocalizedString(@"QMUIButton_Normal_Button_Title", @"按钮,支持高亮背景色") forState:UIControlStateNormal]; + self.normalButton.subtitle = @"支持副标题"; [self.view addSubview:self.normalButton]; self.separatorLayer = [CALayer qmui_separatorLayer]; @@ -39,7 +41,7 @@ - (void)initSubviews { // 图片+文字按钮 self.imagePositionButton1 = [[QMUIButton alloc] init]; - self.imagePositionButton1.tintColorAdjustsTitleAndImage = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + self.imagePositionButton1.tintColorAdjustsTitleAndImage = UIColor.qd_tintColor; self.imagePositionButton1.imagePosition = QMUIButtonImagePositionTop;// 将图片位置改为在文字上方 self.imagePositionButton1.spacingBetweenImageAndTitle = 8; [self.imagePositionButton1 setImage:UIImageMake(@"icon_emotion") forState:UIControlStateNormal]; @@ -49,7 +51,7 @@ - (void)initSubviews { [self.view addSubview:self.imagePositionButton1]; self.imagePositionButton2 = [[QMUIButton alloc] init]; - self.imagePositionButton2.tintColorAdjustsTitleAndImage = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + self.imagePositionButton2.tintColorAdjustsTitleAndImage = UIColor.qd_tintColor; self.imagePositionButton2.imagePosition = QMUIButtonImagePositionBottom;// 将图片位置改为在文字下方 self.imagePositionButton2.spacingBetweenImageAndTitle = 8; [self.imagePositionButton2 setImage:UIImageMake(@"icon_emotion") forState:UIControlStateNormal]; @@ -72,7 +74,7 @@ - (void)viewDidLayoutSubviews { self.separatorLayer.frame = CGRectFlatMake(0, contentMinY + buttonSpacingHeight - PixelOne, CGRectGetWidth(self.view.bounds), PixelOne); // 边框按钮 - self.borderedButton.frame = CGRectSetY(self.normalButton.frame, CGRectGetMaxY(self.separatorLayer.frame) + CGFloatGetCenter(buttonSpacingHeight, CGRectGetHeight(self.normalButton.frame))); + self.borderedButton.frame = CGRectSetXY(self.borderedButton.frame, CGFloatGetCenter(CGRectGetWidth(self.view.bounds), CGRectGetWidth(self.borderedButton.frame)), CGRectGetMaxY(self.separatorLayer.frame) + CGFloatGetCenter(buttonSpacingHeight, CGRectGetHeight(self.normalButton.frame))); // 图片+文字按钮 self.imagePositionButton1.frame = CGRectFlatMake(0, contentMinY + buttonSpacingHeight * 2, CGRectGetWidth(self.view.bounds) / 2.0, buttonSpacingHeight); diff --git a/qmuidemo/Modules/Demos/UIKit/QDObjectMethodsListViewController.m b/qmuidemo/Modules/Demos/UIKit/QDObjectMethodsListViewController.m index 86249b23..8c766412 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDObjectMethodsListViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDObjectMethodsListViewController.m @@ -15,8 +15,6 @@ @interface QDObjectMethodsListViewController () @property(nonatomic, strong) NSMutableArray *> *selectorNames; @property(nonatomic, strong) NSMutableArray *indexesString; @property(nonatomic, strong) NSMutableArray *searchResults; - -@property(nonatomic, assign) UIStatusBarStyle statusBarStyle; @end @implementation QDObjectMethodsListViewController @@ -80,13 +78,6 @@ - (instancetype)initWithClass:(Class)aClass { return self; } -- (instancetype)initWithStyle:(UITableViewStyle)style { - if (self = [super initWithStyle:style]) { - self.statusBarStyle = [super preferredStatusBarStyle]; - } - return self; -} - - (void)initTableView { [super initTableView]; self.tableView.rowHeight = 50; @@ -105,10 +96,6 @@ - (BOOL)isIvarSection:(NSInteger)section { return self.ivarNames.count > 0 && ((self.properties.count > 0 && section == 1) || (self.properties.count <=0 && section == 0)); } -- (UIStatusBarStyle)preferredStatusBarStyle { - return self.statusBarStyle; -} - #pragma mark - - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { @@ -141,7 +128,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N if (tableView == self.tableView) { cell.textLabel.font = CodeFontMake(14); - cell.textLabel.textColor = UIColorGray1; + cell.textLabel.textColor = TableViewCellTitleLabelColor; NSString *name = nil; if ([self isPropertiesSection:indexPath.section]) { name = self.properties[indexPath.row]; @@ -211,9 +198,9 @@ - (void)searchController:(QMUISearchController *)searchController updateResultsF [highlightedLocation addObject:@(location)]; } if (lastLocation != NSNotFound) { - NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:obj attributes:@{NSFontAttributeName: CodeFontMake(14), NSForegroundColorAttributeName: UIColorGray1}]; + NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:obj attributes:@{NSFontAttributeName: CodeFontMake(14), NSForegroundColorAttributeName: TableViewCellTitleLabelColor}]; for (NSInteger i = 0; i < highlightedLocation.count; i++) { - [result addAttribute:NSForegroundColorAttributeName value:[QDThemeManager sharedInstance].currentTheme.themeCodeColor range:NSMakeRange(highlightedLocation[i].unsignedIntegerValue, 1)]; + [result addAttribute:NSForegroundColorAttributeName value:QDThemeManager.currentTheme.themeCodeColor range:NSMakeRange(highlightedLocation[i].unsignedIntegerValue, 1)]; } [self.searchResults addObject:result]; } @@ -230,14 +217,4 @@ - (void)searchController:(QMUISearchController *)searchController updateResultsF [searchController.tableView reloadData]; } -- (void)willPresentSearchController:(QMUISearchController *)searchController { - self.statusBarStyle = UIStatusBarStyleDefault; - [self setNeedsStatusBarAppearanceUpdate]; -} - -- (void)willDismissSearchController:(QMUISearchController *)searchController { - self.statusBarStyle = [super preferredStatusBarStyle]; - [self setNeedsStatusBarAppearanceUpdate]; -} - @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDObjectViewController.m b/qmuidemo/Modules/Demos/UIKit/QDObjectViewController.m index 30cd2ec2..592fcc86 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDObjectViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDObjectViewController.m @@ -23,7 +23,7 @@ - (instancetype)initWithStyle:(UITableViewStyle)style { self.autocompletionClasses = [[NSMutableArray alloc] init]; - dispatch_async(dispatch_get_global_queue(0, 0), ^{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ self.allClasses = [self allClassesArray]; }); } @@ -103,9 +103,13 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N cell.textLabel.numberOfLines = 0; } NSString *className = self.autocompletionClasses[indexPath.row]; + NSString *superclassName = NSStringFromClass(class_getSuperclass(NSClassFromString(className))); + NSString *totalName = superclassName ? [NSString qmui_stringByConcat:className, @" : ", superclassName, nil] : className; NSRange matchingRange = [className.lowercaseString rangeOfString:self.searchBar.text.lowercaseString]; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:className attributes:@{NSFontAttributeName: CodeFontMake(14), NSForegroundColorAttributeName: UIColorGray1}]; - [attributedString addAttribute:NSForegroundColorAttributeName value:[QDThemeManager sharedInstance].currentTheme.themeTintColor range:matchingRange]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:totalName attributes:@{NSFontAttributeName: CodeFontMake(14)}]; + [attributedString addAttribute:NSForegroundColorAttributeName value:UIColor.qd_mainTextColor range:NSMakeRange(0, className.length)]; + [attributedString addAttribute:NSForegroundColorAttributeName value:UIColor.qd_descriptionTextColor range:NSMakeRange(className.length, totalName.length - className.length)]; + [attributedString addAttribute:NSForegroundColorAttributeName value:UIColor.qd_tintColor range:matchingRange]; cell.textLabel.attributedText = attributedString; [cell updateCellAppearanceWithIndexPath:indexPath]; return cell; diff --git a/qmuidemo/Modules/Demos/UIKit/QDOrientationViewController.h b/qmuidemo/Modules/Demos/UIKit/QDOrientationViewController.h index b9bacc17..c871ea78 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDOrientationViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDOrientationViewController.h @@ -8,6 +8,10 @@ #import "QDCommonTableViewController.h" +/** + 更详细的屏幕方向控制可查看 GitHub Wiki:《适用于 iOS 16 及以下版本的屏幕方向控制方式》 + https://github.com/Tencent/QMUI_iOS/wiki/%E9%80%82%E7%94%A8%E4%BA%8E-iOS-16-%E5%8F%8A%E4%BB%A5%E4%B8%8B%E7%89%88%E6%9C%AC%E7%9A%84%E5%B1%8F%E5%B9%95%E6%96%B9%E5%90%91%E6%8E%A7%E5%88%B6%E6%96%B9%E5%BC%8F + */ @interface QDOrientationViewController : QDCommonTableViewController @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDOrientationViewController.m b/qmuidemo/Modules/Demos/UIKit/QDOrientationViewController.m index 66acebb4..db2a2d71 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDOrientationViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDOrientationViewController.m @@ -8,7 +8,9 @@ #import "QDOrientationViewController.h" -const NSInteger kIdentifierForDoneCell = 999; +const NSInteger kIdentifierForNextCell = 997; +const NSInteger kIdentifierForCurrentCell = 998; +const NSInteger kIdentifierForCurrentSwitchCell = 999; @interface QDOrientationViewController () @@ -24,8 +26,8 @@ - (instancetype)init { - (void)initTableView { [super initTableView]; self.tableView.qmui_staticCellDataSource = [[QMUIStaticTableViewCellDataSource alloc] initWithCellDataSections:@[ - // section 0 - @[({ + // section 0 + @[({ QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; d.identifier = UIInterfaceOrientationMaskPortrait; d.text = @"UIInterfaceOrientationMaskPortrait"; @@ -34,7 +36,7 @@ - (void)initTableView { d.accessoryType = QMUIStaticTableViewCellAccessoryTypeCheckmark; d; }), - ({ + ({ QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; d.identifier = UIInterfaceOrientationMaskLandscapeLeft; d.text = @"UIInterfaceOrientationMaskLandscapeLeft"; @@ -43,7 +45,7 @@ - (void)initTableView { d.accessoryType = QMUIStaticTableViewCellAccessoryTypeCheckmark; d; }), - ({ + ({ QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; d.identifier = UIInterfaceOrientationMaskLandscapeRight; d.text = @"UIInterfaceOrientationMaskLandscapeRight"; @@ -52,7 +54,7 @@ - (void)initTableView { d.accessoryType = QMUIStaticTableViewCellAccessoryTypeCheckmark; d; }), - ({ + ({ QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; d.identifier = UIInterfaceOrientationMaskPortraitUpsideDown; d.text = @"UIInterfaceOrientationMaskPortraitUpsideDown"; @@ -61,30 +63,72 @@ - (void)initTableView { d.accessoryType = QMUIStaticTableViewCellAccessoryTypeCheckmark; d; })], - - // section 1 - @[ - ({ + + // section 1 + @[ + ({ QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; - d.identifier = kIdentifierForDoneCell; - d.text = @"完成方向选择,进入该界面"; + d.identifier = kIdentifierForNextCell; + d.text = @"打开符合上述方向的新界面"; + d.accessoryType = QMUIStaticTableViewCellAccessoryTypeDisclosureIndicator; d.didSelectTarget = self; - d.didSelectAction = @selector(handleDoneCellEvent:); + d.didSelectAction = @selector(handleNextCellEvent:); d.cellForRowBlock = ^(UITableView *tableView, __kindof QMUITableViewCell *cell, QMUIStaticTableViewCellData *cellData) { - cell.textLabel.textAlignment = NSTextAlignmentCenter; - cell.textLabel.textColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + cell.textLabel.textColor = UIColor.qd_tintColor; }; d; - })]]]; + }), + ], + + // section 2 + @[ + ({ + QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; + d.identifier = kIdentifierForCurrentCell; + d.text = @"将已选方向设置到当前界面"; + d.didSelectTarget = self; + d.didSelectAction = @selector(handleCurrentCellEvent:); + d.cellForRowBlock = ^(UITableView *tableView, __kindof QMUITableViewCell *cell, QMUIStaticTableViewCellData *cellData) { + cell.textLabel.textColor = UIColor.qd_tintColor; + }; + d; + }), + ({ + QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; + d.identifier = kIdentifierForCurrentSwitchCell; + d.text = @"自动恢复为全方向以测试物理旋转"; + d.accessoryType = QMUIStaticTableViewCellAccessoryTypeSwitch; + d.accessoryValueObject = @YES; + d.accessorySwitchBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData, UISwitch * _Nonnull switcher) { + cellData.accessoryValueObject = @(switcher.on); + }; + d; + }), + ], + ]]; - self.orientationLabel = [[QMUILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorGray7]; - self.orientationLabel.attributedText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"当前界面支持的方向:\n%@", [self descriptionStringWithOrientationMask:self.supportedOrientationMask]] attributes:@{NSFontAttributeName: UIFontMake(14), NSForegroundColorAttributeName: UIColorGray7, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:22 lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentCenter]}]; + self.orientationLabel = [[QMUILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_descriptionTextColor]; self.orientationLabel.numberOfLines = 2; self.orientationLabel.contentEdgeInsets = UIEdgeInsetsMake(24, 24, 24, 24); - [self.orientationLabel sizeToFit]; + [self updateCurrentOrientationDescription]; self.tableView.tableFooterView = self.orientationLabel; } +- (void)updateCurrentOrientationDescription { + self.orientationLabel.attributedText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"当前界面支持的方向:\n%@", [self descriptionStringWithOrientationMask:self.supportedOrientationMask]] attributes:@{NSFontAttributeName: UIFontMake(14), NSForegroundColorAttributeName: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:22 lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentCenter]}]; + [self.view setNeedsLayout]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + CGFloat oldHeight = self.orientationLabel.qmui_height; + self.orientationLabel.frame = CGRectMake(CGRectGetMinX(self.orientationLabel.frame), CGRectGetMinY(self.orientationLabel.frame), CGRectGetWidth(self.tableView.bounds), QMUIViewSelfSizingHeight); + if (self.orientationLabel.qmui_height != oldHeight) { + self.tableView.tableFooterView = nil; + self.tableView.tableFooterView = self.orientationLabel; + } +} + #pragma mark - - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { @@ -95,7 +139,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { if (section == 0) { - return @"请为下一个界面选择支持的设备方向"; + return @"选择支持的设备方向"; } return nil; } @@ -114,7 +158,7 @@ - (void)handleCheckmarkEvent:(QMUIStaticTableViewCellData *)cellData { [self.tableView deselectRowAtIndexPath:cellData.indexPath animated:YES]; } -- (void)handleDoneCellEvent:(QMUIStaticTableViewCellData *)cellData { +- (void)handleNextCellEvent:(QMUIStaticTableViewCellData *)cellData { UIInterfaceOrientationMask mask = 0; for (QMUIStaticTableViewCellData *cellData in self.tableView.qmui_staticCellDataSource.cellDataSections.firstObject) { if (cellData.accessoryType == QMUIStaticTableViewCellAccessoryTypeCheckmark) { @@ -130,6 +174,36 @@ - (void)handleDoneCellEvent:(QMUIStaticTableViewCellData *)cellData { [self.navigationController pushViewController:viewController animated:YES]; } +- (void)handleCurrentCellEvent:(QMUIStaticTableViewCellData *)cellData { + [self.tableView deselectRowAtIndexPath:cellData.indexPath animated:YES]; + + UIInterfaceOrientationMask mask = 0; + for (QMUIStaticTableViewCellData *cellData in self.tableView.qmui_staticCellDataSource.cellDataSections.firstObject) { + if (cellData.accessoryType == QMUIStaticTableViewCellAccessoryTypeCheckmark) { + mask |= cellData.identifier; + } + } + + self.supportedOrientationMask = mask; + [self updateCurrentOrientationDescription]; + [self qmui_setNeedsUpdateOfSupportedInterfaceOrientations]; + + BOOL shouldResetToAll = ((NSNumber *)self.tableView.qmui_staticCellDataSource.cellDataSections[2][1].accessoryValueObject).boolValue; + if (shouldResetToAll) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + self.supportedOrientationMask = UIInterfaceOrientationMaskAll; + [self updateCurrentOrientationDescription]; + // 改为支持全部方向后要主动刷新一下,才能确保设备从横屏物理旋转为竖屏时能自动响应 + [self qmui_setNeedsUpdateOfSupportedInterfaceOrientations]; + + [self.tableView.qmui_staticCellDataSource.cellDataSections[0] enumerateObjectsUsingBlock:^(QMUIStaticTableViewCellData * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + obj.accessoryType = QMUIStaticTableViewCellAccessoryTypeCheckmark; + }]; + [self.tableView reloadData]; + }); + } +} + #pragma mark - Tools - (NSString *)descriptionStringWithOrientationMask:(UIInterfaceOrientationMask)mask { diff --git a/qmuidemo/Modules/Demos/UIKit/QDSearchBarViewController.h b/qmuidemo/Modules/Demos/UIKit/QDSearchBarViewController.h new file mode 100644 index 00000000..0b8b952c --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDSearchBarViewController.h @@ -0,0 +1,17 @@ +// +// QDSearchBarViewController.h +// qmuidemo +// +// Created by MoLice on 2020/7/7. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDCommonTableViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDSearchBarViewController : QDCommonTableViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/UIKit/QDSearchBarViewController.m b/qmuidemo/Modules/Demos/UIKit/QDSearchBarViewController.m new file mode 100644 index 00000000..fd918b49 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDSearchBarViewController.m @@ -0,0 +1,168 @@ +// +// QDSearchBarViewController.m +// qmuidemo +// +// Created by MoLice on 2020/7/7. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDSearchBarViewController.h" + +@interface QDSearchBarViewController () + +@property(nonatomic, assign) BOOL animated; +@end + +@implementation QDSearchBarViewController + +- (void)didInitializeWithStyle:(UITableViewStyle)style { + [super didInitializeWithStyle:style]; + self.shouldShowSearchBar = YES; + self.animated = YES; +} + +- (void)initSearchController { + [super initSearchController]; + self.searchBar.qmui_leftAccessoryView = [[UIImageView alloc] initWithImage:[UIImage qmui_imageWithStrokeColor:[QDCommonUI randomThemeColor] size:CGSizeMake(30, 30) lineWidth:3 cornerRadius:6]]; + self.searchBar.qmui_rightAccessoryView = [[UIImageView alloc] initWithImage:[UIImage qmui_imageWithStrokeColor:[QDCommonUI randomThemeColor] size:CGSizeMake(30, 30) lineWidth:3 cornerRadius:6]]; + self.searchBar.qmui_leftAccessoryViewMargins = UIEdgeInsetsMake(0, 8, 0, 0); + self.searchBar.qmui_rightAccessoryViewMargins = UIEdgeInsetsMake(0, 0, 0, 8); + + // 为了 Demo 效果,先隐藏再让用户手动显示 + self.searchBar.qmui_showsLeftAccessoryView = NO; + self.searchBar.qmui_showsRightAccessoryView = NO; +} + +- (void)initTableView { + [super initTableView]; + __weak __typeof(self)weakSelf = self; + self.tableView.qmui_staticCellDataSource = [[QMUIStaticTableViewCellDataSource alloc] initWithCellDataSections:@[ + @[ + ({ + QMUIStaticTableViewCellData *data = [[QMUIStaticTableViewCellData alloc] init]; + data.identifier = 4; + data.text = @"placeholder 居中"; + data.accessoryType = QMUIStaticTableViewCellAccessoryTypeSwitch; + data.accessoryValueObject = @(weakSelf.searchBar.qmui_centerPlaceholder); + data.accessorySwitchBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData, UISwitch * _Nonnull switcher) { + weakSelf.searchBar.qmui_centerPlaceholder = switcher.on; + }; + data.cellForRowBlock = ^(UITableView * _Nonnull tableView, __kindof QMUITableViewCell * _Nonnull cell, QMUIStaticTableViewCellData * _Nonnull cellData) { + ((UISwitch *)cell.accessoryView).on = weakSelf.searchBar.qmui_centerPlaceholder; + }; + data; + }), + ({ + QMUIStaticTableViewCellData *data = [[QMUIStaticTableViewCellData alloc] init]; + data.identifier = 5; + data.text = @"更换 placeholder 文字"; + data.didSelectBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData) { + weakSelf.searchBar.placeholder = @"很长很长很长的 placeholder"; + [tableView qmui_clearsSelection]; + }; + data; + }) + ], + @[ + ({ + QMUIStaticTableViewCellData *data = [[QMUIStaticTableViewCellData alloc] init]; + data.identifier = 6; + data.text = @"调整默认状态输入框布局"; + data.didSelectBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData) { + [tableView qmui_clearsSelection]; + weakSelf.searchBar.qmui_textFieldMarginsBlock = ^UIEdgeInsets(__kindof UISearchBar * _Nonnull searchBar, BOOL active) { + if (active) { + return UIEdgeInsetsZero; + } else { + return UIEdgeInsetsMake(0, 32, 0, 32); + } + }; + }; + data; + }), + ({ + QMUIStaticTableViewCellData *data = [[QMUIStaticTableViewCellData alloc] init]; + data.identifier = 7; + data.text = @"调整搜索状态取消按钮布局"; + data.didSelectBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData) { + [tableView qmui_clearsSelection]; + weakSelf.searchBar.qmui_cancelButtonMarginsBlock = ^UIEdgeInsets(__kindof UISearchBar * _Nonnull searchBar, BOOL active) { + if (active) { + return UIEdgeInsetsMake(0, -16, 0, 0); + } + return UIEdgeInsetsZero; + }; + if (!weakSelf.searchController.active) { + weakSelf.searchController.active = YES; + } + }; + data; + }) + ], + @[ + ({ + QMUIStaticTableViewCellData *data = [[QMUIStaticTableViewCellData alloc] init]; + data.identifier = 0; + data.text = @"以动画形式展示"; + data.accessoryType = QMUIStaticTableViewCellAccessoryTypeSwitch; + data.accessoryValueObject = @(weakSelf.animated); + data.accessorySwitchBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData, UISwitch * _Nonnull switcher) { + weakSelf.animated = switcher.on; + }; + data.cellForRowBlock = ^(UITableView * _Nonnull tableView, __kindof QMUITableViewCell * _Nonnull cell, QMUIStaticTableViewCellData * _Nonnull cellData) { + ((UISwitch *)cell.accessoryView).on = weakSelf.animated; + }; + data; + }), + ({ + QMUIStaticTableViewCellData *data = [[QMUIStaticTableViewCellData alloc] init]; + data.identifier = 1; + data.text = @"显示 cancelButton"; + data.cellForRowBlock = ^(UITableView * _Nonnull tableView, __kindof QMUITableViewCell * _Nonnull cell, QMUIStaticTableViewCellData * _Nonnull cellData) { + cell.textLabel.text = [NSString stringWithFormat:@"%@ cancelButton", weakSelf.searchBar.showsCancelButton ? @"隐藏" : @"显示"]; + }; + data.didSelectBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData) { + [weakSelf.searchBar setShowsCancelButton:!weakSelf.searchBar.showsCancelButton animated:weakSelf.animated]; + [tableView reloadRowsAtIndexPaths:@[cellData.indexPath] withRowAnimation:UITableViewRowAnimationFade]; + }; + data; + }), + ({ + QMUIStaticTableViewCellData *data = [[QMUIStaticTableViewCellData alloc] init]; + data.identifier = 2; + data.text = @"显示 leftAccessoryView"; + data.cellForRowBlock = ^(UITableView * _Nonnull tableView, __kindof QMUITableViewCell * _Nonnull cell, QMUIStaticTableViewCellData * _Nonnull cellData) { + cell.textLabel.text = [NSString stringWithFormat:@"%@ leftAccessoryView", weakSelf.searchBar.qmui_showsLeftAccessoryView ? @"隐藏" : @"显示"]; + }; + data.didSelectBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData) { + // 显示/隐藏 leftAccessoryView + [weakSelf.searchBar qmui_setShowsLeftAccessoryView:!weakSelf.searchBar.qmui_showsLeftAccessoryView animated:weakSelf.animated]; + [tableView reloadRowsAtIndexPaths:@[cellData.indexPath] withRowAnimation:UITableViewRowAnimationFade]; + }; + data; + }), + ({ + QMUIStaticTableViewCellData *data = [[QMUIStaticTableViewCellData alloc] init]; + data.identifier = 3; + data.text = @"显示 rightAccessoryView"; + data.cellForRowBlock = ^(UITableView * _Nonnull tableView, __kindof QMUITableViewCell * _Nonnull cell, QMUIStaticTableViewCellData * _Nonnull cellData) { + cell.textLabel.text = [NSString stringWithFormat:@"%@ rightAccessoryView", weakSelf.searchBar.qmui_showsRightAccessoryView ? @"隐藏" : @"显示"]; + }; + data.didSelectBlock = ^(UITableView * _Nonnull tableView, QMUIStaticTableViewCellData * _Nonnull cellData) { + [weakSelf.searchBar qmui_setShowsRightAccessoryView:!weakSelf.searchBar.qmui_showsRightAccessoryView animated:weakSelf.animated]; + [tableView reloadRowsAtIndexPaths:@[cellData.indexPath] withRowAnimation:UITableViewRowAnimationFade]; + }; + data; + }), + ]]]; +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { + return @[ + @"Placeholder", + @"Layout", + @"AccessoryView", + ][section]; +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDSearchViewController.m b/qmuidemo/Modules/Demos/UIKit/QDSearchViewController.m index 3c123f4d..64e1a168 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDSearchViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDSearchViewController.m @@ -19,9 +19,9 @@ @implementation QDRecentSearchView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { - self.backgroundColor = UIColorWhite; + self.backgroundColor = UIColor.qd_backgroundColor; - self.titleLabel = [[QMUILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorGray2]; + self.titleLabel = [[QMUILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; self.titleLabel.text = @"最近搜索"; self.titleLabel.contentEdgeInsets = UIEdgeInsetsMake(0, 0, 8, 0); [self.titleLabel sizeToFit]; @@ -36,7 +36,7 @@ - (instancetype)initWithFrame:(CGRect)frame { NSArray *suggestions = @[@"Helps", @"Maintain", @"Liver", @"Health", @"Function", @"Supports", @"Healthy", @"Fat"]; for (NSInteger i = 0; i < suggestions.count; i++) { - QMUIGhostButton *button = [[QMUIGhostButton alloc] initWithGhostType:QMUIGhostButtonColorGray]; + QMUIButton *button = [QDUIHelper generateGhostButtonWithColor:UIColor.qd_tintColor]; [button setTitle:suggestions[i] forState:UIControlStateNormal]; button.titleLabel.font = UIFontMake(14); button.contentEdgeInsets = UIEdgeInsetsMake(6, 20, 6, 20); @@ -48,7 +48,7 @@ - (instancetype)initWithFrame:(CGRect)frame { - (void)layoutSubviews { [super layoutSubviews]; - UIEdgeInsets padding = UIEdgeInsetsConcat(UIEdgeInsetsMake(26, 26, 26, 26), self.qmui_safeAreaInsets); + UIEdgeInsets padding = UIEdgeInsetsConcat(UIEdgeInsetsMake(26, 26, 26, 26), self.safeAreaInsets); CGFloat titleLabelMarginTop = 20; self.titleLabel.frame = CGRectMake(padding.left, padding.top, CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(padding), CGRectGetHeight(self.titleLabel.frame)); @@ -63,7 +63,6 @@ @interface QDSearchViewController () @property(nonatomic, strong) NSArray *keywords; @property(nonatomic, strong) NSMutableArray *searchResultsKeywords; @property(nonatomic, strong) QMUISearchController *mySearchController; -@property(nonatomic, assign) UIStatusBarStyle statusBarStyle; @end @implementation QDSearchViewController @@ -76,8 +75,6 @@ - (instancetype)initWithStyle:(UITableViewStyle)style { self.keywords = @[@"Helps", @"Maintain", @"Liver", @"Health", @"Function", @"Supports", @"Healthy", @"Fat", @"Metabolism", @"Nuturally"]; self.searchResultsKeywords = [[NSMutableArray alloc] init]; - - self.statusBarStyle = [super preferredStatusBarStyle]; } return self; } @@ -87,22 +84,19 @@ - (void)viewDidLoad { // QMUISearchController 有两种使用方式,一种是独立使用,一种是集成到 QMUICommonTableViewController 里使用。为了展示它的使用方式,这里使用第一种,不理会 QMUICommonTableViewController 内部自带的 QMUISearchController self.mySearchController = [[QMUISearchController alloc] initWithContentsViewController:self]; self.mySearchController.searchResultsDelegate = self; + self.mySearchController.supportsSwipeToDismissSearch = YES; self.mySearchController.launchView = [[QDRecentSearchView alloc] init];// launchView 会自动布局,无需处理 frame self.mySearchController.searchBar.qmui_usedAsTableHeaderView = YES;// 以 tableHeaderView 的方式使用 searchBar 的话,将其置为 YES,以辅助兼容一些系统 bug self.tableView.tableHeaderView = self.mySearchController.searchBar; } -- (UIStatusBarStyle)preferredStatusBarStyle { - return self.statusBarStyle; -} - #pragma mark - - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (tableView == self.tableView) { return self.keywords.count; } - return self.searchResultsKeywords.count; + return self.searchResultsKeywords.count * 20; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { @@ -115,11 +109,11 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N if (tableView == self.tableView) { cell.textLabel.text = self.keywords[indexPath.row]; } else { - NSString *keyword = self.searchResultsKeywords[indexPath.row]; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:keyword attributes:@{NSForegroundColorAttributeName: [UIColor blackColor]}]; + NSString *keyword = self.searchResultsKeywords[indexPath.row / 20]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:keyword attributes:@{NSForegroundColorAttributeName: TableViewCellTitleLabelColor}]; NSRange range = [keyword rangeOfString:self.mySearchController.searchBar.text]; if (range.location != NSNotFound) { - [attributedString addAttributes:@{NSForegroundColorAttributeName: [QDThemeManager sharedInstance].currentTheme.themeTintColor} range:range]; + [attributedString addAttributes:@{NSForegroundColorAttributeName: UIColor.qd_tintColor} range:range]; } cell.textLabel.attributedText = attributedString; } @@ -152,14 +146,4 @@ - (void)searchController:(QMUISearchController *)searchController updateResultsF } } -- (void)willPresentSearchController:(QMUISearchController *)searchController { - self.statusBarStyle = UIStatusBarStyleDefault; - [self setNeedsStatusBarAppearanceUpdate]; -} - -- (void)willDismissSearchController:(QMUISearchController *)searchController { - self.statusBarStyle = [super preferredStatusBarStyle]; - [self setNeedsStatusBarAppearanceUpdate]; -} - @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDSliderViewController.m b/qmuidemo/Modules/Demos/UIKit/QDSliderViewController.m index 81fc8ad5..643d9db8 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDSliderViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDSliderViewController.m @@ -7,67 +7,115 @@ // #import "QDSliderViewController.h" +#import "QMUIInteractiveDebugger.h" @interface QDSliderViewController () -@property(nonatomic, strong) QMUISlider *slider; -@property(nonatomic, strong) UISlider *systemSlider; -@property(nonatomic, strong) UILabel *label1; -@property(nonatomic, strong) UILabel *label2; +@property(nonatomic, strong) UISlider *slider; +@property(nonatomic, strong) QMUIInteractiveDebugPanelViewController *debugViewController; @end @implementation QDSliderViewController - (void)initSubviews { [super initSubviews]; - self.slider = [[QMUISlider alloc] init]; - self.slider.value = .3; - self.slider.minimumTrackTintColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; - self.slider.maximumTrackTintColor = UIColorGray9; - self.slider.trackHeight = 1;// 支持修改背后导轨的高度 - self.slider.thumbColor = self.slider.minimumTrackTintColor; - self.slider.thumbSize = CGSizeMake(14, 14);// 支持修改拖拽圆点的大小 - - // 支持修改圆点的阴影样式 - self.slider.thumbShadowColor = [self.slider.minimumTrackTintColor colorWithAlphaComponent:.3]; - self.slider.thumbShadowOffset = CGSizeMake(0, 2); - self.slider.thumbShadowRadius = 3; + __weak __typeof(self)weakSelf = self; + self.slider = [[UISlider alloc] init]; + self.slider.value = .3; + self.slider.minimumTrackTintColor = UIColor.qd_tintColor; + self.slider.maximumTrackTintColor = UIColor.qd_separatorColor; + self.slider.qmui_trackHeight = 1; // 圆点背后那条槽的高度 + self.slider.qmui_thumbSize = CGSizeMake(14, 14); // 圆点的大小 + self.slider.qmui_thumbColor = self.slider.minimumTrackTintColor; // 圆点的填充颜色 + self.slider.qmui_thumbShadow = [NSShadow qmui_shadowWithColor:self.slider.minimumTrackTintColor shadowOffset:CGSizeZero shadowRadius:5]; // 圆点的阴影 + self.slider.qmui_stepControlConfiguration = ^(__kindof UISlider * _Nonnull slider, QMUISliderStepControl * _Nonnull stepControl, NSUInteger index) { + stepControl.indicator.backgroundColor = slider.qmui_thumbColor; + }; + self.slider.qmui_stepDidChangeBlock = ^(__kindof UISlider * _Nonnull slider, NSUInteger precedingStep) { + ((QMUITextField *)weakSelf.debugViewController.debugItems[1].actionView).text = [NSString stringWithFormat:@"%@", @(slider.qmui_step)]; + }; // 监听 step 的变化(用系统的 UIControlEventValueChanged 也可以,具体请看 UISlider+QMUI.h 的注释)。 [self.view addSubview:self.slider]; - self.systemSlider = [[UISlider alloc] init]; - self.systemSlider.minimumTrackTintColor = self.slider.minimumTrackTintColor; - self.systemSlider.maximumTrackTintColor = self.slider.maximumTrackTintColor; - self.systemSlider.thumbTintColor = self.slider.minimumTrackTintColor; - self.systemSlider.value = self.slider.value; - [self.view addSubview:self.systemSlider]; - - self.label1 = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:TableViewSectionHeaderTextColor]; - self.label1.text = @"QMUISlider"; - [self.label1 sizeToFit]; - [self.view addSubview:self.label1]; - - self.label2 = [[UILabel alloc] init]; - [self.label2 qmui_setTheSameAppearanceAsLabel:self.label1]; - self.label2.text = @"UISlider"; - [self.label2 sizeToFit]; - [self.view addSubview:self.label2]; + [self generateDebugViewController]; +} + +- (void)generateDebugViewController { + __weak __typeof(self)weakSelf = self; + self.debugViewController = [QDUIHelper generateDebugViewControllerWithTitle:@"输入新的值" items:@[ + [QMUIInteractiveDebugPanelItem boolItemWithTitle:@"steps" valueGetter:^(UISwitch * _Nonnull actionView) { + actionView.on = weakSelf.slider.qmui_numberOfSteps >= 2; + } valueSetter:^(UISwitch * _Nonnull actionView) { + BOOL hasAddedStepItem = NO; + for (QMUIInteractiveDebugPanelItem *item in weakSelf.debugViewController.debugItems) { + if ([item.title isEqualToString:@"step"]) { + hasAddedStepItem = YES; + break; + } + } + + if (actionView.on) { + weakSelf.slider.qmui_numberOfSteps = 5; + weakSelf.slider.qmui_stepControlConfiguration = ^(UISlider *slider, QMUISliderStepControl * _Nonnull stepControl, NSUInteger index) { + stepControl.titleLabel.text = [NSString stringWithFormat:@"第%@档", @(index)]; + }; + + if (!hasAddedStepItem) { + [weakSelf.debugViewController insertDebugItem:[QMUIInteractiveDebugPanelItem numbericItemWithTitle:@"step" valueGetter:^(QMUITextField * _Nonnull actionView) { + actionView.text = [NSString stringWithFormat:@"%@", @(weakSelf.slider.qmui_step)]; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + weakSelf.slider.qmui_step = actionView.text.integerValue; + }] atIndex:1]; + [weakSelf.view setNeedsLayout]; + } + + weakSelf.slider.qmui_step = 3; + } else { + weakSelf.slider.qmui_numberOfSteps = 0; + if (hasAddedStepItem) { + [weakSelf.debugViewController removeDebugItemAtIndex:1]; + [weakSelf.view setNeedsLayout]; + } + } + }], + [QMUIInteractiveDebugPanelItem numbericItemWithTitle:@"trackHeight" valueGetter:^(QMUITextField * _Nonnull actionView) { + actionView.text = [NSString stringWithFormat:@"%.2f", weakSelf.slider.qmui_trackHeight]; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + weakSelf.slider.qmui_trackHeight = actionView.text.doubleValue; + }], + [QMUIInteractiveDebugPanelItem numbericItemWithTitle:@"thumbSize" valueGetter:^(QMUITextField * _Nonnull actionView) { + actionView.text = [NSString stringWithFormat:@"%.2f", weakSelf.slider.qmui_thumbSize.height]; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + weakSelf.slider.qmui_thumbSize = CGSizeMake(actionView.text.doubleValue, actionView.text.doubleValue); + }], + [QMUIInteractiveDebugPanelItem colorItemWithTitle:@"thumbShadowColor" valueGetter:^(QMUITextField * _Nonnull actionView) { + actionView.text = ((UIColor *)weakSelf.slider.qmui_thumbShadow.shadowColor).qmui_RGBAString; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + NSShadow *shadow = weakSelf.slider.qmui_thumbShadow; + shadow.shadowColor = [UIColor qmui_colorWithRGBAString:actionView.text]; + weakSelf.slider.qmui_thumbShadow = shadow; + }], + [QMUIInteractiveDebugPanelItem numbericItemWithTitle:@"outsideEdge" valueGetter:^(QMUITextField * _Nonnull actionView) { + actionView.text = [NSString stringWithFormat:@"%.2f", weakSelf.slider.qmui_outsideEdge.left]; + } valueSetter:^(QMUITextField * _Nonnull actionView) { + CGFloat outside = actionView.text.doubleValue; + weakSelf.slider.qmui_outsideEdge = UIEdgeInsetsMake(outside, outside, outside, outside); + }], + ]]; + [self.view addSubview:self.debugViewController.view]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - UIEdgeInsets padding = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.qmui_safeAreaInsets.left, 24 + self.view.qmui_safeAreaInsets.bottom, 24 + self.view.qmui_safeAreaInsets.right); - - self.label1.frame = CGRectSetXY(self.label1.frame, padding.left, padding.top); - + UIEdgeInsets padding = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.safeAreaInsets.left, 24 + self.view.safeAreaInsets.bottom, 24 + self.view.safeAreaInsets.right); + CGFloat contentWidth = MIN(CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding), 426); + CGFloat minX = CGFloatGetCenter(CGRectGetWidth(self.view.bounds), contentWidth); [self.slider sizeToFit]; - self.slider.frame = CGRectMake(padding.left, CGRectGetMaxY(self.label1.frame) + 16, CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding), CGRectGetHeight(self.slider.frame)); - - self.label2.frame = CGRectSetXY(self.label2.frame, padding.left, CGRectGetMaxY(self.slider.frame) + 64); + self.slider.frame = CGRectMake(minX, padding.top + 16, contentWidth, CGRectGetHeight(self.slider.frame)); - [self.systemSlider sizeToFit]; - self.systemSlider.frame = CGRectSetY(self.slider.frame, CGRectGetMaxY(self.label2.frame) + 16); + CGSize size = [self.debugViewController contentSizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)]; + self.debugViewController.view.frame = CGRectMake(minX, CGRectGetMaxY(self.slider.frame) + 36, contentWidth, size.height); } @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTabBarItemViewController.h b/qmuidemo/Modules/Demos/UIKit/QDTabBarDemoViewController.h similarity index 63% rename from qmuidemo/Modules/Demos/UIKit/QDTabBarItemViewController.h rename to qmuidemo/Modules/Demos/UIKit/QDTabBarDemoViewController.h index a78462cf..e0371b2d 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDTabBarItemViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDTabBarDemoViewController.h @@ -1,5 +1,5 @@ // -// QDTabBarItemViewController.h +// QDTabBarDemoViewController.h // qmuidemo // // Created by QMUI Team on 2016/10/9. @@ -8,6 +8,6 @@ #import "QDCommonListViewController.h" -@interface QDTabBarItemViewController : QDCommonListViewController +@interface QDTabBarDemoViewController : QDCommonListViewController @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTabBarDemoViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTabBarDemoViewController.m new file mode 100644 index 00000000..23a00a19 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDTabBarDemoViewController.m @@ -0,0 +1,134 @@ +// +// QDTabBarDemoViewController.m +// qmuidemo +// +// Created by QMUI Team on 2016/10/9. +// Copyright © 2016年 QMUI Team. All rights reserved. +// + +#import "QDTabBarDemoViewController.h" + +@interface QDTabBarDemoViewController () + +@property(nonatomic, strong) UITabBar *tabBar; +@property(nonatomic, strong) UIView *blurTestView; +@property(nonatomic, strong) UIView *blurTestView2; +@end + +@implementation QDTabBarDemoViewController + +- (void)initSubviews { + [super initSubviews]; + + // 双击 tabBarItem 的回调 + __weak __typeof(self)weakSelf = self; + void (^tabBarItemDoubleTapBlock)(UITabBarItem *tabBarItem, NSInteger index) = ^(UITabBarItem *tabBarItem, NSInteger index) { + [QMUITips showInfo:[NSString stringWithFormat:@"双击了第 %@ 个 tab", @(index + 1)] inView:weakSelf.view hideAfterDelay:1.2]; + }; + + self.tabBar = [[UITabBar alloc] init]; + + UITabBarItem *item1 = [QDUIHelper tabBarItemWithTitle:@"QMUIKit" image:[UIImageMake(@"icon_tabbar_uikit") imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] selectedImage:UIImageMake(@"icon_tabbar_uikit_selected") tag:0]; + item1.qmui_doubleTapBlock = tabBarItemDoubleTapBlock; + + UITabBarItem *item2 = [QDUIHelper tabBarItemWithTitle:@"Components" image:[UIImageMake(@"icon_tabbar_component") imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] selectedImage:UIImageMake(@"icon_tabbar_component_selected") tag:1]; + item2.qmui_doubleTapBlock = tabBarItemDoubleTapBlock; + + UITabBarItem *item3 = [QDUIHelper tabBarItemWithTitle:@"Lab" image:[UIImageMake(@"icon_tabbar_lab") imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] selectedImage:UIImageMake(@"icon_tabbar_lab_selected") tag:2]; + item3.qmui_doubleTapBlock = tabBarItemDoubleTapBlock; + + self.tabBar.items = @[item1, item2, item3]; + self.tabBar.selectedItem = item1; + [self.tabBar sizeToFit]; + [self.view addSubview:self.tabBar]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + CGFloat tabBarHeight = TabBarHeight; + self.tabBar.frame = CGRectMake(0, CGRectGetHeight(self.view.bounds) - tabBarHeight, CGRectGetWidth(self.view.bounds), tabBarHeight); + if (self.blurTestView) { + CGRect rect = [self.tableView convertRect:self.tabBar.frame fromView:self.view]; + self.blurTestView.frame = CGRectMake(100, CGRectGetMinY(rect) - 25, CGRectGetWidth(self.tableView.bounds) - 100 * 2, 25 * 2); + } + if (self.blurTestView2) { + self.blurTestView2.frame = CGRectMake(100, - 25, CGRectGetWidth(self.tableView.bounds) - 100 * 2, 25 * 2); + } +} + +- (void)initDataSource { + self.dataSourceWithDetailText = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"双击 UITabBarItem 可触发双击事件", @"点击本 cell 可重新设置 title,触发 item 重新构造", + @"可获取 UITabBarItem 上的 imageView", @"例如这里拿到 imageView 后会做动画", + @"可精准指定 UITabBar 的磨砂和前景色", @"兼容所有 iOS 版本。而系统仅在 iOS 13 及以后才提供 backgroundEffect 的修改方式,且系统的 UIVisualEffectView 在展示一些 UIBlurEffectStyle 时会强制加一个前景色,导致业务再叠加的前景色效果不可控,因此 QMUI 提供接口可以屏蔽系统的前景色,只显示业务的,从而达到精准控制设计效果的作用。", + nil]; +} + +- (void)didSelectCellWithTitle:(NSString *)title { + + if ([title isEqualToString:@"双击 UITabBarItem 可触发双击事件"]) { + // 修改 title 会导致 UITabBar 重新构造 view,要确保这种场景也能让 doubleTap 生效 + self.tabBar.items.firstObject.title = @"QMUIKit 2"; + } else if ([title isEqualToString:@"可获取 UITabBarItem 上的 imageView"]) { + // 注意只有在 UITabBar 可见的时候才能获取到这个 view,如果一初始化 UITabBarItem 就立马获取,是获取不到的。 + UIImageView *imageViewInTabBarItem = self.tabBar.items.firstObject.qmui_imageView; + if (imageViewInTabBarItem) { + [UIView animateWithDuration:.25 delay:0 usingSpringWithDamping:.1 initialSpringVelocity:5 options:QMUIViewAnimationOptionsCurveOut animations:^{ + imageViewInTabBarItem.transform = CGAffineTransformMakeScale(1.4, 1.4); + } completion:^(BOOL finished) { + imageViewInTabBarItem.transform = CGAffineTransformIdentity; + }]; + } + } else if ([title isEqualToString:@"可精准指定 UITabBar 的磨砂和前景色"]) { + + // backgroundImage 优先级比 backgroundEffect 高,所以这里主动把 backgroundImage 清理掉 + self.tabBar.backgroundImage = nil; + self.tabBar.barTintColor = nil; + + NSArray *effectStyles = @[ + @(UIBlurEffectStyleExtraLight), + @(UIBlurEffectStyleLight), + @(UIBlurEffectStyleDark), + @(UIBlurEffectStyleProminent), + @(UIBlurEffectStyleSystemUltraThinMaterialLight), + @(UIBlurEffectStyleSystemMaterialLight), + @(UIBlurEffectStyleSystemChromeMaterialLight), + @(UIBlurEffectStyleSystemUltraThinMaterialDark), + @(UIBlurEffectStyleSystemMaterialDark), + @(UIBlurEffectStyleSystemChromeMaterialDark), + ]; + + UIBlurEffectStyle style = effectStyles[arc4random() % effectStyles.count].integerValue; + UIBlurEffect *effect = [UIBlurEffect effectWithStyle:style]; + self.tabBar.qmui_effect = effect; + self.tabBar.qmui_effectForegroundColor = [UIColor.qd_tintColor colorWithAlphaComponent:.3]; + + // 为了展示磨砂效果,tabBar 背后垫一个 view 来查看透过磨砂的样子 + if (!self.blurTestView) { + self.blurTestView = UIView.new; + self.blurTestView.backgroundColor = UIColor.qd_tintColor; + [self.tableView addSubview:self.blurTestView]; + [self.view setNeedsLayout]; + } + + UINavigationBar *navigationBar = self.navigationController.navigationBar; + [navigationBar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault]; + navigationBar.barTintColor = nil; + [navigationBar setNeedsLayout]; + [navigationBar layoutIfNeeded]; + navigationBar.qmui_effect = effect; + navigationBar.qmui_effectForegroundColor = [UIColor.qd_tintColor colorWithAlphaComponent:.3]; + + // 为了展示磨砂效果,tabBar 背后垫一个 view 来查看透过磨砂的样子 + if (!self.blurTestView2) { + self.blurTestView2 = UIView.new; + self.blurTestView2.backgroundColor = UIColor.qd_tintColor; + [self.tableView addSubview:self.blurTestView2]; + [self.view setNeedsLayout]; + } + } + + [self.tableView qmui_clearsSelection]; +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTabBarItemViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTabBarItemViewController.m deleted file mode 100644 index 94759213..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDTabBarItemViewController.m +++ /dev/null @@ -1,73 +0,0 @@ -// -// QDTabBarItemViewController.m -// qmuidemo -// -// Created by QMUI Team on 2016/10/9. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QDTabBarItemViewController.h" - -@interface QDTabBarItemViewController () - -@property(nonatomic, strong) UITabBar *tabBar; -@end - -@implementation QDTabBarItemViewController - -- (void)initSubviews { - [super initSubviews]; - - // 双击 tabBarItem 的回调 - __weak __typeof(self)weakSelf = self; - void (^tabBarItemDoubleTapBlock)(UITabBarItem *tabBarItem, NSInteger index) = ^(UITabBarItem *tabBarItem, NSInteger index) { - [QMUITips showInfo:[NSString stringWithFormat:@"双击了第 %@ 个 tab", @(index + 1)] inView:weakSelf.view hideAfterDelay:1.2]; - }; - - self.tabBar = [[UITabBar alloc] init]; - self.tabBar.tintColor = TabBarTintColor; - - UITabBarItem *item1 = [QDUIHelper tabBarItemWithTitle:@"QMUIKit" image:[UIImageMake(@"icon_tabbar_uikit") imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] selectedImage:UIImageMake(@"icon_tabbar_uikit_selected") tag:0]; - item1.qmui_doubleTapBlock = tabBarItemDoubleTapBlock; - - UITabBarItem *item2 = [QDUIHelper tabBarItemWithTitle:@"Components" image:[UIImageMake(@"icon_tabbar_component") imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] selectedImage:UIImageMake(@"icon_tabbar_component_selected") tag:1]; - item2.qmui_doubleTapBlock = tabBarItemDoubleTapBlock; - - UITabBarItem *item3 = [QDUIHelper tabBarItemWithTitle:@"Lab" image:[UIImageMake(@"icon_tabbar_lab") imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] selectedImage:UIImageMake(@"icon_tabbar_lab_selected") tag:2]; - item3.qmui_doubleTapBlock = tabBarItemDoubleTapBlock; - - self.tabBar.items = @[item1, item2, item3]; - self.tabBar.selectedItem = item1; - [self.tabBar sizeToFit]; - [self.view addSubview:self.tabBar]; -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - CGFloat tabBarHeight = TabBarHeight; - self.tabBar.frame = CGRectMake(0, CGRectGetHeight(self.view.bounds) - tabBarHeight, CGRectGetWidth(self.view.bounds), tabBarHeight); -} - -- (void)initDataSource { - self.dataSource = @[@"双击 UITabBarItem 可触发双击事件", - @"可获取 UITabBarItem 上的 imageView"]; -} - -- (void)didSelectCellWithTitle:(NSString *)title { - - if ([title isEqualToString:@"可获取 UITabBarItem 上的 imageView"]) { - // 注意只有在 UITabBar 可见的时候才能获取到这个 view,如果一初始化 UITabBarItem 就立马获取,是获取不到的。 - UIImageView *imageViewInTabBarItem = self.tabBar.items.firstObject.qmui_imageView; - if (imageViewInTabBarItem) { - [UIView animateWithDuration:.25 delay:0 usingSpringWithDamping:.1 initialSpringVelocity:5 options:QMUIViewAnimationOptionsCurveOut animations:^{ - imageViewInTabBarItem.transform = CGAffineTransformMakeScale(1.4, 1.4); - } completion:^(BOOL finished) { - imageViewInTabBarItem.transform = CGAffineTransformIdentity; - }]; - } - } - - [self.tableView qmui_clearsSelection]; -} - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellReorderStyleViewController.h b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellReorderStyleViewController.h new file mode 100644 index 00000000..9dec0a20 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellReorderStyleViewController.h @@ -0,0 +1,17 @@ +// +// QDTableViewCellReorderStyleViewController.h +// qmuidemo +// +// Created by molice on 2022/12/8. +// Copyright © 2022 QMUI Team. All rights reserved. +// + +#import "QDCommonTableViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDTableViewCellReorderStyleViewController : QDCommonTableViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellReorderStyleViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellReorderStyleViewController.m new file mode 100644 index 00000000..41ed192b --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellReorderStyleViewController.m @@ -0,0 +1,72 @@ +// +// QDTableViewCellReorderStyleViewController.m +// qmuidemo +// +// Created by molice on 2022/12/8. +// Copyright © 2022 QMUI Team. All rights reserved. +// + +#import "QDTableViewCellReorderStyleViewController.h" + +@interface QDTableViewCellReorderStyleViewController () + +@end + +@implementation QDTableViewCellReorderStyleViewController + +- (instancetype)init { + return [self initWithStyle:UITableViewStyleGrouped]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + self.tableView.editing = YES; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 3; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return 10; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *identifier = @"cell"; + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; + if (!cell) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; + [cell qmui_styledAsQMUITableViewCell]; + cell.qmui_configureReorderingStyleBlock = ^(__kindof UITableView * _Nonnull tableView, __kindof UITableViewCell * _Nonnull aCell, BOOL isReordering) { + aCell.layer.qmui_shadow = isReordering ? [NSShadow qmui_shadowWithColor:[UIColorRed colorWithAlphaComponent:.3] shadowOffset:CGSizeMake(0, 4) shadowRadius:12] : nil; + }; + } + cell.textLabel.text = [NSString stringWithFormat:@"section%@-row%@", @(indexPath.section), @(indexPath.row)]; + return cell; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + return TableViewCellNormalHeight; +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { + return [NSString stringWithFormat:@"section%@", @(section)]; +} + +- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { + return UITableViewCellEditingStyleNone; +} + +- (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath { + return NO; +} + +- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { + return YES; +} + +- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { + +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellSeparatorInsetsViewController.h b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellSeparatorInsetsViewController.h new file mode 100644 index 00000000..6f5e6681 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellSeparatorInsetsViewController.h @@ -0,0 +1,17 @@ +// +// QDTableViewCellSeparatorInsetsViewController.h +// qmuidemo +// +// Created by MoLice on 2020/6/30. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDStyleSelectableTableViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDTableViewCellSeparatorInsetsViewController : QDStyleSelectableTableViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellSeparatorInsetsViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellSeparatorInsetsViewController.m new file mode 100644 index 00000000..c653c3b1 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellSeparatorInsetsViewController.m @@ -0,0 +1,99 @@ +// +// QDTableViewCellSeparatorInsetsViewController.m +// qmuidemo +// +// Created by MoLice on 2020/6/30. +// Copyright © 2020 QMUI Team. All rights reserved. +// + +#import "QDTableViewCellSeparatorInsetsViewController.h" + +@implementation QDTableViewCellSeparatorInsetsViewController + +#pragma mark - + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 3; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return section + 1; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *identifier = @"cell"; + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; + if (!cell) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + cell.textLabel.font = UIFontMake(16); + cell.textLabel.textColor = TableViewCellTitleLabelColor; + cell.qmui_separatorInsetsBlock = ^UIEdgeInsets(__kindof UITableView * _Nonnull aTableView, __kindof UITableViewCell * _Nonnull aCell) { + QMUITableViewCellPosition position = aCell.qmui_cellPosition; + CGFloat defaultRight = 20; + switch (aTableView.style) { + case UITableViewStylePlain: { + CGRect frame = [aCell convertRect:aCell.textLabel.bounds fromView:aCell.textLabel]; + CGFloat left = CGRectGetMinX(frame); + CGFloat right = aCell.qmui_accessoryView ? CGRectGetWidth(aCell.bounds) - CGRectGetMinX(aCell.qmui_accessoryView.frame) : defaultRight; + return UIEdgeInsetsMake(0, left, 0, right); + } + case UITableViewStyleGrouped: { + CGRect frame = [aCell convertRect:aCell.textLabel.bounds fromView:aCell.textLabel]; + CGFloat left = (position & QMUITableViewCellPositionLastInSection) == QMUITableViewCellPositionLastInSection ? 0 : CGRectGetMinX(frame); + CGFloat right = aCell.qmui_accessoryView ? CGRectGetWidth(aCell.bounds) - CGRectGetMinX(aCell.qmui_accessoryView.frame) : defaultRight; + right = (position & QMUITableViewCellPositionLastInSection) == QMUITableViewCellPositionLastInSection ? 0 : right; + return UIEdgeInsetsMake(0, left, 0, right); + } + default: { + // InsetGrouped + if ((position & QMUITableViewCellPositionLastInSection) == QMUITableViewCellPositionLastInSection) { + return QMUITableViewCellSeparatorInsetsNone; + } + CGRect frame = [aCell convertRect:aCell.textLabel.bounds fromView:aCell.textLabel]; + CGFloat left = CGRectGetMinX(frame); + CGFloat right = aCell.qmui_accessoryView ? CGRectGetWidth(aCell.bounds) - CGRectGetMinX(aCell.qmui_accessoryView.frame) : defaultRight; + return UIEdgeInsetsMake(0, left, 0, right); + } + } + }; + cell.qmui_topSeparatorInsetsBlock = ^UIEdgeInsets(__kindof UITableView * _Nonnull aTableView, __kindof UITableViewCell * _Nonnull aCell) { + if (aTableView.style == UITableViewStyleGrouped && aCell.qmui_cellPosition & QMUITableViewCellPositionFirstInSection) { + return UIEdgeInsetsZero; + } + return QMUITableViewCellSeparatorInsetsNone; + }; + } + + NSString *text = nil; + + if (indexPath.section > 0 && indexPath.row == 0) { + cell.accessoryType = UITableViewCellAccessoryDetailButton; + text = @"分隔线在 accessoryView 前截止"; + } else { + cell.accessoryType = UITableViewCellAccessoryNone; + } + if (indexPath.section == 2 && indexPath.row == 1) { + cell.imageView.image = [UIImage qmui_imageWithStrokeColor:[QDCommonUI randomThemeColor] size:CGSizeMake(30, 30) lineWidth:3 cornerRadius:6]; + text = @"分隔线在 imageView 之后开始"; + } else { + cell.imageView.image = nil; + } + + if (!text) { + QMUITableViewCellPosition position = [tableView qmui_positionForRowAtIndexPath:indexPath]; + if ((position & QMUITableViewCellPositionSingleInSection) == QMUITableViewCellPositionSingleInSection) { + text = @"section 单行的情况"; + } else if ((position & QMUITableViewCellPositionLastInSection) == QMUITableViewCellPositionLastInSection) { + text = @"section 最后一行的情况"; + } + } + cell.textLabel.text = text; + return cell; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + return TableViewCellNormalHeight; +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellViewController.h b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellViewController.h deleted file mode 100644 index ee3e69ec..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellViewController.h +++ /dev/null @@ -1,14 +0,0 @@ -// -// QDTableViewCellViewController.h -// qmui -// -// Created by QMUI Team on 14/11/5. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QDCommonListViewController.h" - -/// 展示 QMUITableViewCell 能力的 demo 列表 -@interface QDTableViewCellViewController : QDCommonListViewController - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellViewController.m deleted file mode 100644 index fb5096f5..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellViewController.m +++ /dev/null @@ -1,32 +0,0 @@ -// -// QDTableViewCellViewController.m -// qmui -// -// Created by QMUI Team on 14/11/5. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QDTableViewCellViewController.h" -#import "QDTableViewCellInsetsViewController.h" -#import "QDTableViewCellAccessoryTypeViewController.h" - -@implementation QDTableViewCellViewController - -- (void)initDataSource { - self.dataSource = @[@"通过 insets 系列属性调整间距", - @"通过配置表修改 accessoryType 的样式"]; -} - -- (void)didSelectCellWithTitle:(NSString *)title { - [self.tableView qmui_clearsSelection]; - UIViewController *viewController = nil; - if ([title isEqualToString:@"通过 insets 系列属性调整间距"]) { - viewController = [[QDTableViewCellInsetsViewController alloc] init]; - } else if ([title isEqualToString:@"通过配置表修改 accessoryType 的样式"]) { - viewController = [[QDTableViewCellAccessoryTypeViewController alloc] init]; - } - viewController.title = title; - [self.navigationController pushViewController:viewController animated:YES]; -} - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewController.h b/qmuidemo/Modules/Demos/UIKit/QDTableViewController.h deleted file mode 100644 index 1813a1a3..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDTableViewController.h +++ /dev/null @@ -1,14 +0,0 @@ -// -// QDTableViewController.h -// qmuidemo -// -// Created by QMUI Team on 2017/11/7. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#import "QDCommonListViewController.h" - -/// QMUIKit 首页那个 tableView 的 demo 列表 -@interface QDTableViewController : QDCommonListViewController - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTableViewController.m deleted file mode 100644 index d5dc8f19..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDTableViewController.m +++ /dev/null @@ -1,35 +0,0 @@ -// -// QDTableViewController.m -// qmuidemo -// -// Created by QMUI Team on 2017/11/7. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#import "QDTableViewController.h" -#import "QDTableViewCellViewController.h" -#import "QDTableViewHeaderFooterViewController.h" - -@interface QDTableViewController () - -@end - -@implementation QDTableViewController - -- (void)initDataSource { - self.dataSource = @[@"QMUITableViewCell", - @"QMUITableViewHeaderFooterView"]; -} - -- (void)didSelectCellWithTitle:(NSString *)title { - UIViewController *viewController = nil; - if ([title isEqualToString:@"QMUITableViewCell"]) { - viewController = [[QDTableViewCellViewController alloc] init]; - } else if ([title isEqualToString:@"QMUITableViewHeaderFooterView"]) { - viewController = [[QDTableViewHeaderFooterViewController alloc] init]; - } - viewController.title = title; - [self.navigationController pushViewController:viewController animated:YES]; -} - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewHeaderFooterViewController.h b/qmuidemo/Modules/Demos/UIKit/QDTableViewHeaderFooterViewController.h index a1051df1..5ecfbc9b 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDTableViewHeaderFooterViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDTableViewHeaderFooterViewController.h @@ -6,9 +6,9 @@ // Copyright © 2017年 QMUI Team. All rights reserved. // -#import "QDCommonTableViewController.h" +#import "QDStyleSelectableTableViewController.h" /// 展示 QMUITableViewHeaderFooterView 以及 UITableView (QMUI) 里与 section header 相关运算的 demo -@interface QDTableViewHeaderFooterViewController : QDCommonTableViewController +@interface QDTableViewHeaderFooterViewController : QDStyleSelectableTableViewController @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewHeaderFooterViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTableViewHeaderFooterViewController.m index 8cfcf4d0..4e7786f6 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDTableViewHeaderFooterViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDTableViewHeaderFooterViewController.m @@ -47,6 +47,11 @@ - (void)handleButtonEvent:(UIView *)view { // 下面这堆代码都不用看,主要看上面的 handle 方法 +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [self.debugView renderWithTableView:self.tableView];// 进入界面过程中触发的那一次 scrollViewDidScroll: 还不是最终的状态,所以在 didAppear 时主动刷新一遍 +} + #pragma mark - - (void)scrollViewDidScroll:(UIScrollView *)scrollView { @@ -60,6 +65,7 @@ - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + if (section == 2) return 0; return 3; } @@ -173,7 +179,7 @@ - (void)renderWithTableView:(UITableView *)tableView { - (void)layoutSubviews { [super layoutSubviews]; - UIEdgeInsets padding = UIEdgeInsetsConcat(UIEdgeInsetsMake(24, 24, 24, 24), self.qmui_safeAreaInsets); + UIEdgeInsets padding = UIEdgeInsetsConcat(UIEdgeInsetsMake(24, 24, 24, 24), self.safeAreaInsets); NSArray *leftLabels = @[self.visibleHeadersLabel, self.pinnedHeaderLabel, self.headerPinnedLabel]; NSArray *rightLabels = @[self.visibleHeadersValue, self.pinnedHeaderValue, self.headerPinnedValue]; diff --git a/qmuidemo/Modules/Demos/UIKit/QDTextFieldViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTextFieldViewController.m index e94661cf..5687e788 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDTextFieldViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDTextFieldViewController.m @@ -16,46 +16,54 @@ @interface QDTextFieldViewController () @implementation QDTextFieldViewController -- (void)didInitialize { - [super didInitialize]; - // https://github.com/Tencent/QMUI_iOS/issues/114 - self.automaticallyAdjustsScrollViewInsets = NO; -} - - (void)initSubviews { [super initSubviews]; _textField = [[QMUITextField alloc] init]; self.textField.delegate = self; - self.textField.maximumTextLength = 10; - self.textField.placeholder = @"请输入文字"; + self.textField.maximumTextLength = 11; + self.textField.placeholder = @"请输入手机号码"; self.textField.font = UIFontMake(16); self.textField.layer.cornerRadius = 2; self.textField.layer.borderColor = UIColorSeparator.CGColor; self.textField.layer.borderWidth = PixelOne; self.textField.textInsets = UIEdgeInsetsMake(0, 10, 0, 10); self.textField.clearButtonMode = UITextFieldViewModeAlways; + self.textField.qmui_respondsToDeleteActionAtLeading = YES; [self.view addSubview:self.textField]; self.tipsLabel = [[UILabel alloc] init]; - self.tipsLabel.attributedText = [[NSAttributedString alloc] initWithString:@"支持:\n1. 自定义 placeholder 颜色;\n2. 修改 clearButton 的图片和布局位置;\n3. 调整输入框与文字之间的间距;\n4. 限制可输入的最大文字长度(可试试输入 emoji、从中文输入法候选词输入等);\n5. 计算文字长度时区分中英文。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColorGray6, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]}]; + self.tipsLabel.attributedText = [[NSAttributedString alloc] initWithString:@"支持:\n1. 自定义 placeholder 颜色;\n2. 修改 clearButton 的图片和布局位置;\n3. 调整输入框与文字之间的间距;\n4. 限制可输入的最大文字长度(可试试输入 emoji、从中文输入法候选词输入等);\n5. 计算文字长度时区分中英文。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]}]; self.tipsLabel.numberOfLines = 0; [self.view addSubview:self.tipsLabel]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - UIEdgeInsets padding = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator + 16, 16 + self.view.qmui_safeAreaInsets.left, 16 + self.view.qmui_safeAreaInsets.bottom, 16 + self.view.qmui_safeAreaInsets.right); + UIEdgeInsets padding = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator + 16, 16 + self.view.safeAreaInsets.left, 16 + self.view.safeAreaInsets.bottom, 16 + self.view.safeAreaInsets.right); CGFloat contentWidth = CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding); self.textField.frame = CGRectMake(padding.left, padding.top, contentWidth, 40); self.tipsLabel.frame = CGRectFlatMake(padding.left, CGRectGetMaxY(self.textField.frame) + 8, contentWidth, QMUIViewSelfSizingHeight); } +- (BOOL)shouldHideKeyboardWhenTouchInView:(UIView *)view { + return YES; +} + #pragma mark - +- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string originalValue:(BOOL)originalValue { + BOOL isDeleting = !string.length; + if (!isDeleting && ![string qmui_stringMatchedByPattern:@"^\\d+$"]) { + [QMUITips showWithText:@"仅允许输入数字" inView:self.view hideAfterDelay:1.0]; + return NO; + } + return originalValue; +} + - (void)textField:(QMUITextField *)textField didPreventTextChangeInRange:(NSRange)range replacementString:(NSString *)replacementString { - [QMUITips showWithText:[NSString stringWithFormat:@"文字不能超过 %@ 个字符", @(textField.maximumTextLength)] inView:self.view hideAfterDelay:2.0]; + [QMUITips showWithText:[NSString stringWithFormat:@"最多仅允许输入%@位", @(textField.maximumTextLength)] inView:self.view hideAfterDelay:1.5]; } @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTextViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTextViewController.m index dafc1b95..a77e5c90 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDTextViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDTextViewController.m @@ -20,7 +20,6 @@ @implementation QDTextViewController - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - self.automaticallyAdjustsScrollViewInsets = NO; self.textViewMinimumHeight = 96; } return self; @@ -29,6 +28,7 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB - (void)initSubviews { [super initSubviews]; self.textView = [[QMUITextView alloc] init]; + self.textView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; self.textView.delegate = self; self.textView.placeholder = @"支持 placeholder、支持自适应高度、支持限制文本输入长度"; self.textView.placeholderColor = UIColorPlaceholder; // 自定义 placeholder 的颜色 @@ -36,8 +36,10 @@ - (void)initSubviews { self.textView.returnKeyType = UIReturnKeySend; self.textView.enablesReturnKeyAutomatically = YES; self.textView.typingAttributes = @{NSFontAttributeName: UIFontMake(15), - NSForegroundColorAttributeName: UIColorGray1, - NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]}; + NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20], + NSForegroundColorAttributeName: self.textView.textColor + }; + self.textView.backgroundColor = UIColor.qd_backgroundColorLighten; // 限制可输入的字符长度 self.textView.maximumTextLength = 100; @@ -50,14 +52,14 @@ - (void)initSubviews { [self.view addSubview:self.textView]; self.tipsLabel = [[UILabel alloc] init]; - self.tipsLabel.attributedText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"最长不超过 %@ 个文字,可尝试输入 emoji、粘贴一大段文字。\n会自动监听回车键,触发发送逻辑。", @(self.textView.maximumTextLength)] attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColorGray6, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:16]}]; + self.tipsLabel.attributedText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"最长不超过 %@ 个文字,可尝试输入 emoji、粘贴一大段文字。\n会自动监听回车键,触发发送逻辑。", @(self.textView.maximumTextLength)] attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:16]}]; self.tipsLabel.numberOfLines = 0; [self.view addSubview:self.tipsLabel]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - UIEdgeInsets padding = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator + 16, 16 + self.view.qmui_safeAreaInsets.left, 16 + self.view.qmui_safeAreaInsets.bottom, 16 + self.view.qmui_safeAreaInsets.right); + UIEdgeInsets padding = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator + 16, 16 + self.view.safeAreaInsets.left, 16 + self.view.safeAreaInsets.bottom, 16 + self.view.safeAreaInsets.right); CGFloat contentWidth = CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding); CGSize textViewSize = [self.textView sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)]; @@ -85,12 +87,12 @@ - (void)textView:(QMUITextView *)textView newHeightAfterTextChanged:(CGFloat)hei } - (void)textView:(QMUITextView *)textView didPreventTextChangeInRange:(NSRange)range replacementText:(NSString *)replacementText { - [QMUITips showWithText:[NSString stringWithFormat:@"文字不能超过 %@ 个字符", @(textView.maximumTextLength)] inView:self.view hideAfterDelay:2.0]; + [QMUITips showWithText:[NSString stringWithFormat:@"文字不能超过 %@ 个字符", @(textView.maximumTextLength)] inView:self.view hideAfterDelay:.8]; } // 可以利用这个 delegate 来监听发送按钮的事件,当然,如果你习惯以前的方式的话,也可以继续在 textView:shouldChangeTextInRange:replacementText: 里处理 - (BOOL)textViewShouldReturn:(QMUITextView *)textView { - [QMUITips showSucceed:[NSString stringWithFormat:@"成功发送文字:%@", textView.text] inView:self.view hideAfterDelay:3.0]; + [QMUITips showSucceed:[NSString stringWithFormat:@"成功发送文字:%@", textView.text] inView:self.view hideAfterDelay:1]; textView.text = nil; // return YES 表示这次 return 按钮的点击是为了触发“发送”,而不是为了输入一个换行符 diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIKitViewController.m b/qmuidemo/Modules/Demos/UIKit/QDUIKitViewController.m index 9bbae1c9..fa419a44 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDUIKitViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDUIKitViewController.m @@ -7,18 +7,18 @@ // #import "QDUIKitViewController.h" +#import "QDNavigationController.h" +#import "QDCommonListViewController.h" #import "QDColorViewController.h" #import "QDImageViewController.h" #import "QDLabelViewController.h" #import "QDTextViewController.h" #import "QDTextFieldViewController.h" -#import "QDTableViewController.h" #import "QDButtonViewController.h" #import "QDAlertController.h" #import "QDSearchViewController.h" #import "QDNavigationListViewController.h" -#import "QDTabBarItemViewController.h" -#import "QDUIViewQMUIViewController.h" +#import "QDTabBarDemoViewController.h" #import "QDAboutViewController.h" #import "QDObjectViewController.h" #import "QDFontViewController.h" @@ -26,6 +26,18 @@ #import "QDOrientationViewController.h" #import "QDCAAnimationViewController.h" #import "QDImageViewViewController.h" +#import "QDTableViewHeaderFooterViewController.h" +#import "QDInsetGroupedTableViewController.h" +#import "QDUIViewBorderViewController.h" +#import "QDUIViewDebugViewController.h" +#import "QDUIViewLayoutViewController.h" +#import "QDSearchBarViewController.h" +#import "QDTableViewCellInsetsViewController.h" +#import "QDTableViewCellAccessoryTypeViewController.h" +#import "QDTableViewCellSeparatorInsetsViewController.h" +#import "QDControlViewController.h" +#import "QDBlurEffectViewController.h" +#import "QDTableViewCellReorderStyleViewController.h" @implementation QDUIKitViewController @@ -35,26 +47,32 @@ - (void)initDataSource { @"QMUILabel", UIImageMake(@"icon_grid_label"), @"QMUITextView", UIImageMake(@"icon_grid_textView"), @"QMUITextField", UIImageMake(@"icon_grid_textField"), - @"QMUISlider", UIImageMake(@"icon_grid_slider"), + @"UISlider+QMUI", UIImageMake(@"icon_grid_slider"), @"QMUIAlertController", UIImageMake(@"icon_grid_alert"), @"QMUITableView", UIImageMake(@"icon_grid_cell"), - @"QMUISearchController", UIImageMake(@"icon_grid_search"), @"ViewController Orientation", UIImageMake(@"icon_grid_orientation"), @"QMUINavigationController", UIImageMake(@"icon_grid_navigation"), - @"UITabBarItem+QMUI", UIImageMake(@"icon_grid_tabBarItem"), + @"UISearchBar+QMUI", UIImageMake(@"icon_grid_search"), + @"UITabBar+QMUI", UIImageMake(@"icon_grid_tabBar"), @"UIColor+QMUI", UIImageMake(@"icon_grid_color"), @"UIImage+QMUI", UIImageMake(@"icon_grid_image"), @"UIImageView+QMUI", UIImageMake(@"icon_grid_imageView"), @"UIFont+QMUI", UIImageMake(@"icon_grid_font"), + @"UIControl+QMUI", UIImageMake(@"icon_grid_control"), @"UIView+QMUI", UIImageMake(@"icon_grid_view"), @"NSObject+QMUI", UIImageMake(@"icon_grid_nsobject"), @"CAAnimation+QMUI", UIImageMake(@"icon_grid_caanimation"), + @"UIBlurEffect+QMUI", UIImageMake(@"icon_grid_blur"), nil]; } +- (void)didInitialize { + [super didInitialize]; + self.title = @"QMUIKit"; +} + - (void)setupNavigationItems { [super setupNavigationItems]; - self.title = @"QMUIKit"; self.navigationItem.rightBarButtonItem = [UIBarButtonItem qmui_itemWithImage:UIImageMake(@"icon_nav_about") target:self action:@selector(handleAboutItemEvent)]; AddAccessibilityLabel(self.navigationItem.rightBarButtonItem, @"打开关于界面"); } @@ -79,17 +97,80 @@ - (void)didSelectCellWithTitle:(NSString *)title { else if ([title isEqualToString:@"QMUITextField"]) { viewController = [[QDTextFieldViewController alloc] init]; } - else if ([title isEqualToString:@"QMUISlider"]) { + else if ([title isEqualToString:@"UISlider+QMUI"]) { viewController = [[QDSliderViewController alloc] init]; } else if ([title isEqualToString:@"QMUITableView"]) { - viewController = [[QDTableViewController alloc] init]; + viewController = ({ + QDCommonListViewController *vc = [[QDCommonListViewController alloc] init]; + vc.dataSource = @[ + @"(QM)UITableViewCell", + @"QMUITableViewHeaderFooterView", + @"UITableViewStyleInsetGrouped"]; + __weak __typeof(vc)weakVc1 = vc; + vc.didSelectTitleBlock = ^(NSString *title) { + UIViewController *viewController = nil; + if ([title isEqualToString:@"(QM)UITableViewCell"]) { + viewController = ({ + QDCommonListViewController *vc = [[QDCommonListViewController alloc] init]; + vc.dataSource = @[ + @"通过 insets 系列属性调整间距", + @"通过 block 调整分隔线位置", + @"通过 block 调整排序时的样式", + @"通过配置表修改 accessoryType 的样式" + ]; + __weak __typeof(vc)weakVc2 = vc; + vc.didSelectTitleBlock = ^(NSString *title) { + [weakVc2.tableView qmui_clearsSelection]; + UIViewController *viewController = nil; + if ([title isEqualToString:@"通过 insets 系列属性调整间距"]) { + viewController = [[QDTableViewCellInsetsViewController alloc] init]; + } else if ([title isEqualToString:@"通过 block 调整分隔线位置"]) { + viewController = [[QDTableViewCellSeparatorInsetsViewController alloc] init]; + } else if ([title isEqualToString:@"通过 block 调整排序时的样式"]) { + viewController = [[QDTableViewCellReorderStyleViewController alloc] init]; + } else if ([title isEqualToString:@"通过配置表修改 accessoryType 的样式"]) { + viewController = [[QDTableViewCellAccessoryTypeViewController alloc] init]; + } + viewController.title = title; + [weakVc2.navigationController pushViewController:viewController animated:YES]; + }; + vc; + }); + } else if ([title isEqualToString:@"QMUITableViewHeaderFooterView"]) { + viewController = QDTableViewHeaderFooterViewController.new; + } else if ([title isEqualToString:@"UITableViewStyleInsetGrouped"]) { + viewController = QDInsetGroupedTableViewController.new; + } + viewController.title = title; + [weakVc1.navigationController pushViewController:viewController animated:YES]; + }; + vc; + }); } else if ([title isEqualToString:@"QMUIButton"]) { viewController = [[QDButtonViewController alloc] init]; } - else if ([title isEqualToString:@"QMUISearchController"]) { - viewController = [[QDSearchViewController alloc] init]; + else if ([title isEqualToString:@"UISearchBar+QMUI"]) { + viewController = ({ + QDCommonListViewController *vc = QDCommonListViewController.new; + vc.dataSource = @[ + @"UISearchBar(QMUI)", + @"QMUISearchController", + ]; + __weak __typeof(vc)weakVc = vc; + vc.didSelectTitleBlock = ^(NSString *title) { + UIViewController *viewController = nil; + if ([title isEqualToString:@"UISearchBar(QMUI)"]) { + viewController = QDSearchBarViewController.new; + } else if ([title isEqualToString:@"QMUISearchController"]) { + viewController = QDSearchViewController.new; + } + viewController.title = title; + [weakVc.navigationController pushViewController:viewController animated:YES]; + }; + vc; + }); } else if ([title isEqualToString:@"QMUIAlertController"]) { viewController = [[QDAlertController alloc] init]; @@ -100,14 +181,37 @@ - (void)didSelectCellWithTitle:(NSString *)title { else if ([title isEqualToString:@"QMUINavigationController"]) { viewController = [[QDNavigationListViewController alloc] init]; } - else if ([title isEqualToString:@"UITabBarItem+QMUI"]) { - viewController = [[QDTabBarItemViewController alloc] init]; + else if ([title isEqualToString:@"UITabBar+QMUI"]) { + viewController = [[QDTabBarDemoViewController alloc] init]; } else if ([title isEqualToString:@"UIFont+QMUI"]) { viewController = [[QDFontViewController alloc] init]; } + else if ([title isEqualToString:@"UIControl+QMUI"]) { + viewController = [[QDControlViewController alloc] init]; + } else if ([title isEqualToString:@"UIView+QMUI"]) { - viewController = [[QDUIViewQMUIViewController alloc] init]; + viewController = ({ + QDCommonListViewController *vc = [[QDCommonListViewController alloc] init]; + vc.dataSource = @[ + @"UIView (QMUI_Border)", + @"UIView (QMUI_Debug)", + @"UIView (QMUI_Layout)"]; + __weak __typeof(vc)weakVc = vc; + vc.didSelectTitleBlock = ^(NSString *title) { + UIViewController *viewController = nil; + if ([title isEqualToString:@"UIView (QMUI_Border)"]) { + viewController = [[QDUIViewBorderViewController alloc] init]; + } else if ([title isEqualToString:@"UIView (QMUI_Debug)"]) { + viewController = [[QDUIViewDebugViewController alloc] init]; + } else if ([title isEqualToString:@"UIView (QMUI_Layout)"]) { + viewController = [[QDUIViewLayoutViewController alloc] init]; + } + viewController.title = title; + [weakVc.navigationController pushViewController:viewController animated:YES]; + }; + vc; + }); } else if ([title isEqualToString:@"NSObject+QMUI"]) { viewController = [[QDObjectViewController alloc] init]; @@ -115,6 +219,9 @@ - (void)didSelectCellWithTitle:(NSString *)title { else if ([title isEqualToString:@"CAAnimation+QMUI"]) { viewController = [[QDCAAnimationViewController alloc] init]; } + else if ([title isEqualToString:@"UIBlurEffect+QMUI"]) { + viewController = [[QDBlurEffectViewController alloc] init]; + } viewController.title = title; [self.navigationController pushViewController:viewController animated:YES]; } diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIViewBorderViewController.m b/qmuidemo/Modules/Demos/UIKit/QDUIViewBorderViewController.m index aee0566e..debe7ff2 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDUIViewBorderViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDUIViewBorderViewController.m @@ -32,6 +32,9 @@ @interface QDUIViewBorderViewController () @property(nonatomic, strong) QMUILabel *widthTitleLabel; @property(nonatomic, strong) QMUITextField *widthTextField; +@property(nonatomic, strong) QMUILabel *insetsTitleLabel; +@property(nonatomic, strong) QMUITextField *insetsTextField; + @property(nonatomic, strong) QMUILabel *cornerRadiusTitleLabel; @property(nonatomic, strong) QMUITextField *cornerRadiusTextField; @@ -56,7 +59,7 @@ @implementation QDUIViewBorderViewController - (void)initSubviews { [super initSubviews]; self.targetView = [[UIView alloc] qmui_initWithSize:CGSizeMake(100, 100)]; - self.targetView.backgroundColor = [[QDThemeManager sharedInstance].currentTheme.themeTintColor colorWithAlphaComponent:.3]; + self.targetView.backgroundColor = [UIColor.qd_tintColor colorWithAlphaComponent:.3]; [self.view addSubview:self.targetView]; self.scrollView = [[UIScrollView alloc] init]; @@ -81,6 +84,10 @@ - (void)initSubviews { self.widthTitleLabel = [self generateTitleLabelWithText:NSStringFromSelector(@selector(qmui_borderWidth))]; self.widthTextField = [self generateNumericTextField]; + self.insetsTitleLabel = [self generateTitleLabelWithText:NSStringFromSelector(@selector(qmui_borderInsets))]; + self.insetsTextField = [self generateNumericTextField]; + self.insetsTextField.qmui_width = 100; + self.cornerRadiusTitleLabel = [self generateTitleLabelWithText:NSStringFromSelector(@selector(cornerRadius))]; self.cornerRadiusTextField = [self generateNumericTextField]; @@ -100,8 +107,11 @@ - (void)initSubviews { // 默认值的设置 self.locationSegmentedControl.selectedSegmentIndex = 0; self.positionTopButton.selected = YES; - self.widthTextField.text = [NSString stringWithFormat:@"%.1f", 3.0]; - self.cornerRadiusTextField.text = @"0"; + self.widthTextField.text = [NSString stringWithFormat:@"%.1f", 10.0]; + self.insetsTextField.text = @"10 0 0 0"; + self.positionLeftButton.selected = YES; + self.positionTopButton.selected = YES; + self.cornerRadiusTextField.text = @"30"; self.colorSegmentedControl.selectedSegmentIndex = 0; self.dashPatternWidthTextField.text = @"0"; self.dashPatternSpacingTextField.text = @"0"; @@ -152,7 +162,7 @@ - (void)viewDidDisappear:(BOOL)animated { } - (QMUILabel *)generateTitleLabelWithText:(NSString *)text { - QMUILabel *label = [[QMUILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + QMUILabel *label = [[QMUILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; label.text = text; [label sizeToFit]; [self.scrollView addSubview:label]; @@ -161,7 +171,7 @@ - (QMUILabel *)generateTitleLabelWithText:(NSString *)text { - (QMUISegmentedControl *)generateSegmentedControlWithItems:(NSArray *)items { QMUISegmentedControl *segmentedControl = [[QMUISegmentedControl alloc] initWithItems:items]; - segmentedControl.tintColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + segmentedControl.tintColor = UIColor.qd_tintColor; segmentedControl.frame = CGRectSetWidth(segmentedControl.frame, 240);// 统一按照最长的来就行啦 segmentedControl.transform = CGAffineTransformMakeScale(.8, .8); segmentedControl.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; @@ -179,7 +189,7 @@ - (QMUIButton *)generateSelectableButtonWithTitle:(NSString *)title { button.imagePosition = QMUIButtonImagePositionRight; button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill; button.titleLabel.font = UIFontMake(12); - [button setTitleColor:UIColorGray3 forState:UIControlStateNormal]; + [button setTitleColor:UIColor.qd_descriptionTextColor forState:UIControlStateNormal]; button.highlightedBackgroundColor = TableViewCellSelectedBackgroundColor; button.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; [self.scrollView addSubview:button]; @@ -194,7 +204,7 @@ - (QMUITextField *)generateNumericTextField { textField.layer.borderWidth = PixelOne; textField.layer.borderColor = UIColorSeparator.CGColor; textField.textAlignment = NSTextAlignmentCenter; - textField.textColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + textField.textColor = UIColor.qd_tintColor; [self.scrollView addSubview:textField]; [textField addTarget:self action:@selector(handleTextFieldChangedEvent:) forControlEvents:UIControlEventEditingChanged]; return textField; @@ -204,8 +214,8 @@ - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; if (!IS_IPAD && IS_LANDSCAPE) { - self.targetView.qmui_left = self.view.qmui_safeAreaInsets.left + 32; - self.targetView.qmui_top = CGFloatGetCenter(CGRectGetHeight(self.view.bounds) - UIEdgeInsetsGetVerticalValue(self.view.qmui_safeAreaInsets), CGRectGetHeight(self.targetView.frame)); + self.targetView.qmui_left = self.view.safeAreaInsets.left + 32; + self.targetView.qmui_top = CGFloatGetCenter(CGRectGetHeight(self.view.bounds) - UIEdgeInsetsGetVerticalValue(self.view.safeAreaInsets), CGRectGetHeight(self.targetView.frame)); CGFloat scrollViewMinX = CGRectGetMaxX(self.targetView.frame) + 32; self.scrollView.frame = CGRectMake(scrollViewMinX, 0, self.view.qmui_width - scrollViewMinX, CGRectGetHeight(self.view.bounds)); self.scrollView.qmui_borderPosition = QMUIViewBorderPositionLeft; @@ -217,8 +227,8 @@ - (void)viewDidLayoutSubviews { self.scrollView.qmui_borderPosition = QMUIViewBorderPositionTop; } - CGFloat marginLeft = 16 + self.scrollView.qmui_safeAreaInsets.left; - CGFloat marginRight = 16 + self.scrollView.qmui_safeAreaInsets.right; + CGFloat marginLeft = 16 + self.scrollView.safeAreaInsets.left; + CGFloat marginRight = 16 + self.scrollView.safeAreaInsets.right; __block CGFloat maxY = 0; CGFloat defaultLineHeight = 44; @@ -244,6 +254,12 @@ - (void)viewDidLayoutSubviews { self.widthTextField.qmui_top = maxY + CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.widthTextField.frame)); maxY += defaultLineHeight; + self.insetsTitleLabel.qmui_left = marginLeft; + self.insetsTitleLabel.qmui_top = maxY + CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.insetsTitleLabel.frame)); + self.insetsTextField.qmui_right = CGRectGetWidth(self.scrollView.bounds) - marginRight; + self.insetsTextField.qmui_top = maxY + CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.insetsTextField.frame)); + maxY += defaultLineHeight; + self.colorTitleLabel.qmui_left = marginLeft; self.colorTitleLabel.qmui_top = maxY + CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.colorTitleLabel.frame)); self.colorSegmentedControl.center = CGPointMake(CGRectGetWidth(self.scrollView.bounds) - marginRight - CGRectGetWidth(self.colorSegmentedControl.frame) / 2.0, self.colorTitleLabel.center.y); @@ -290,10 +306,10 @@ - (void)handleSegmentedControlEvent:(QMUISegmentedControl *)segmentedControl { UIColor *borderColor = nil; switch (segmentedControl.selectedSegmentIndex) { case 0: - borderColor = [[QDThemeManager sharedInstance].currentTheme.themeTintColor colorWithAlphaComponent:.5]; + borderColor = [UIColor.qd_tintColor colorWithAlphaComponent:.5]; break; case 1: - borderColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; + borderColor = UIColor.qd_tintColor; break; case 2: borderColor = [UIColor blackColor]; @@ -316,6 +332,18 @@ - (void)handleSelectableButtonEvent:(QMUIButton *)button { - (void)handleTextFieldChangedEvent:(QMUITextField *)textField { if (textField == self.widthTextField) { self.targetView.qmui_borderWidth = [textField.text doubleValue]; + } else if (textField == self.insetsTextField) { + NSArray *insetsNumber = [[textField.text.qmui_trim componentsSeparatedByString:@" "] qmui_mapWithBlock:^id _Nonnull(NSString * _Nonnull item, NSInteger index) { + return [NSNumber numberWithDouble:item.doubleValue]; + }]; + if (insetsNumber.count != 4) return; + UIEdgeInsets insets = UIEdgeInsetsMake( + insetsNumber[0].qmui_CGFloatValue, + insetsNumber[1].qmui_CGFloatValue, + insetsNumber[2].qmui_CGFloatValue, + insetsNumber[3].qmui_CGFloatValue + ); + self.targetView.qmui_borderInsets = insets; } else if (textField == self.dashPhaseTextField) { self.targetView.qmui_dashPhase = [textField.text doubleValue]; } else if (textField == self.cornerRadiusTextField) { @@ -364,16 +392,18 @@ - (void)updateMaskedCorners { } if (cornerMask == 0) { // 默认值 - cornerMask = QMUILayerMinXMinYCorner|QMUILayerMaxXMinYCorner|QMUILayerMinXMaxYCorner|QMUILayerMaxXMaxYCorner; + cornerMask = QMUILayerAllCorner; } + BeginIgnoreDeprecatedWarning self.targetView.layer.qmui_maskedCorners = cornerMask; + EndIgnoreDeprecatedWarning } - (void)fireAllEvents { - [@[self.locationSegmentedControl, self.widthTextField, self.colorSegmentedControl, self.dashPatternWidthTextField, self.dashPatternSpacingTextField, self.dashPhaseTextField] enumerateObjectsUsingBlock:^(UIControl *obj, NSUInteger idx, BOOL * _Nonnull stop) { + [@[self.locationSegmentedControl, self.widthTextField, self.insetsTextField, self.colorSegmentedControl, self.dashPatternWidthTextField, self.dashPatternSpacingTextField, self.dashPhaseTextField] enumerateObjectsUsingBlock:^(UIControl *obj, NSUInteger idx, BOOL * _Nonnull stop) { [obj sendActionsForControlEvents:UIControlEventValueChanged]; }]; - [@[self.widthTextField, self.dashPatternWidthTextField, self.dashPatternSpacingTextField, self.dashPhaseTextField] enumerateObjectsUsingBlock:^(UIControl *obj, NSUInteger idx, BOOL * _Nonnull stop) { + [@[self.widthTextField, self.insetsTextField, self.dashPatternWidthTextField, self.dashPatternSpacingTextField, self.dashPhaseTextField] enumerateObjectsUsingBlock:^(UIControl *obj, NSUInteger idx, BOOL * _Nonnull stop) { [obj sendActionsForControlEvents:UIControlEventEditingChanged]; }]; [self updateBorderPosition]; @@ -421,9 +451,9 @@ - (void)handleGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer { CGFloat avoidFingerY = -100; CGFloat magnifyingViewMinX = CGRectGetMinX(self.magnifyingView.frame); CGFloat magnifyingViewMinY = CGRectGetMinY(self.magnifyingView.frame); - if (magnifyingViewMinY + avoidFingerY < self.magnifyingView.superview.qmui_safeAreaInsets.top) { - magnifyingViewMinY = self.magnifyingView.superview.qmui_safeAreaInsets.top; - if (magnifyingViewMinX + avoidFingerX < self.magnifyingView.superview.qmui_safeAreaInsets.left) { + if (magnifyingViewMinY + avoidFingerY < self.magnifyingView.superview.safeAreaInsets.top) { + magnifyingViewMinY = self.magnifyingView.superview.safeAreaInsets.top; + if (magnifyingViewMinX + avoidFingerX < self.magnifyingView.superview.safeAreaInsets.left) { magnifyingViewMinX -= avoidFingerX; } else { magnifyingViewMinX += avoidFingerX; @@ -442,7 +472,7 @@ - (void)keyboardWillChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUser textFieldRect = CGRectSetHeight(textFieldRect, CGRectGetHeight(textFieldRect) + 12);// 12 是距离底部键盘的间距 CGFloat keyboardHeight = [keyboardUserInfo heightInView:self.view]; self.scrollView.contentInset = UIEdgeInsetsSetBottom(self.scrollView.contentInset, keyboardHeight); - self.scrollView.scrollIndicatorInsets = UIEdgeInsetsSetBottom(self.scrollView.contentInset, self.scrollView.contentInset.bottom - (keyboardHeight > 0 ? self.scrollView.qmui_safeAreaInsets.bottom : 0)); + self.scrollView.scrollIndicatorInsets = UIEdgeInsetsSetBottom(self.scrollView.contentInset, self.scrollView.contentInset.bottom - (keyboardHeight > 0 ? self.scrollView.safeAreaInsets.bottom : 0)); if (CGRectGetMaxY(textFieldRect) < CGRectGetHeight(self.view.bounds) - keyboardHeight) { return; } diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIViewDebugViewController.m b/qmuidemo/Modules/Demos/UIKit/QDUIViewDebugViewController.m index 30c812e2..21486485 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDUIViewDebugViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDUIViewDebugViewController.m @@ -21,7 +21,7 @@ @implementation QDUIViewDebugViewController - (void)initSubviews { [super initSubviews]; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"通过 qmui_shouldShowDebugColor 让 UIView 以及其所有的 subviews 都加上一个背景色,方便查看其布局情况" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColorGray1, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:22]}]; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"通过 qmui_shouldShowDebugColor 让 UIView 以及其所有的 subviews 都加上一个背景色,方便查看其布局情况" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColor.qd_mainTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:22]}]; NSDictionary *codeAttributes = CodeAttributes(16); [attributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { [attributedString addAttributes:codeAttributes range:codeRange]; @@ -46,7 +46,7 @@ - (void)initSubviews { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - UIEdgeInsets padding = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.qmui_safeAreaInsets.left, 24 + self.view.qmui_safeAreaInsets.bottom, 24 + self.view.qmui_safeAreaInsets.right); + UIEdgeInsets padding = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.safeAreaInsets.left, 24 + self.view.safeAreaInsets.bottom, 24 + self.view.safeAreaInsets.right); self.descriptionLabel.frame = CGRectMake(padding.left, padding.top, CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding), QMUIViewSelfSizingHeight); self.subview1.qmui_left = 24; diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIViewLayoutViewController.m b/qmuidemo/Modules/Demos/UIKit/QDUIViewLayoutViewController.m index 72480646..e8e6201e 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDUIViewLayoutViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDUIViewLayoutViewController.m @@ -34,7 +34,7 @@ - (void)initSubviews { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - UIEdgeInsets padding = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.qmui_safeAreaInsets.left, 24 + self.view.qmui_safeAreaInsets.bottom, 24 + self.view.qmui_safeAreaInsets.right); + UIEdgeInsets padding = UIEdgeInsetsMake(24 + self.qmui_navigationBarMaxYInViewCoordinator, 24 + self.view.safeAreaInsets.left, 24 + self.view.safeAreaInsets.bottom, 24 + self.view.safeAreaInsets.right); CGFloat contentWidth = CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding); // 所有布局都需要在同一个坐标系里才有效 diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIViewQMUIViewController.h b/qmuidemo/Modules/Demos/UIKit/QDUIViewQMUIViewController.h deleted file mode 100644 index 8e251310..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDUIViewQMUIViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDUIViewQMUIViewController.h -// qmuidemo -// -// Created by QMUI Team on 2016/10/11. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QDCommonListViewController.h" - -@interface QDUIViewQMUIViewController : QDCommonListViewController - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIViewQMUIViewController.m b/qmuidemo/Modules/Demos/UIKit/QDUIViewQMUIViewController.m deleted file mode 100644 index e634321b..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDUIViewQMUIViewController.m +++ /dev/null @@ -1,35 +0,0 @@ -// -// QDUIViewQMUIViewController.m -// qmuidemo -// -// Created by QMUI Team on 2016/10/11. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QDUIViewQMUIViewController.h" -#import "QDUIViewBorderViewController.h" -#import "QDUIViewDebugViewController.h" -#import "QDUIViewLayoutViewController.h" - -@implementation QDUIViewQMUIViewController - -- (void)initDataSource { - self.dataSource = @[@"UIView (QMUI_Border)", - @"UIView (QMUI_Debug)", - @"UIView (QMUI_Layout)"]; -} - -- (void)didSelectCellWithTitle:(NSString *)title { - UIViewController *viewController = nil; - if ([title isEqualToString:@"UIView (QMUI_Border)"]) { - viewController = [[QDUIViewBorderViewController alloc] init]; - } else if ([title isEqualToString:@"UIView (QMUI_Debug)"]) { - viewController = [[QDUIViewDebugViewController alloc] init]; - } else if ([title isEqualToString:@"UIView (QMUI_Layout)"]) { - viewController = [[QDUIViewLayoutViewController alloc] init]; - } - viewController.title = title; - [self.navigationController pushViewController:viewController animated:YES]; -} - -@end diff --git a/qmuidemo/PrefixHeader.pch b/qmuidemo/PrefixHeader.pch index eb4ce8cc..5edb56e0 100644 --- a/qmuidemo/PrefixHeader.pch +++ b/qmuidemo/PrefixHeader.pch @@ -25,5 +25,8 @@ #import "QDCommonUI.h" #import "QDUIHelper.h" #import "QDThemeManager.h" +#import "QDNavigationController.h" +#import "QDCommonViewController.h" +#import "QDCommonTableViewController.h" #endif diff --git a/qmuidemo/Resources/images/image0@2x.png b/qmuidemo/Resources/images/image0@2x.png index 12d180cb..b503304d 100644 Binary files a/qmuidemo/Resources/images/image0@2x.png and b/qmuidemo/Resources/images/image0@2x.png differ diff --git a/qmuidemo/en.lproj/Info.plist b/qmuidemo/en.lproj/Info.plist index 802bb38b..f97745b5 100644 --- a/qmuidemo/en.lproj/Info.plist +++ b/qmuidemo/en.lproj/Info.plist @@ -38,8 +38,6 @@ armv7 - UIStatusBarStyle - UIStatusBarStyleLightContent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -54,7 +52,22 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneDelegateClassName + AppDelegate + UISceneConfigurationName + Default Configuration + + + + diff --git a/qmuidemo/en.lproj/LaunchScreen.strings b/qmuidemo/en.lproj/LaunchScreen.strings index 7e1503ce..6940ef37 100644 --- a/qmuidemo/en.lproj/LaunchScreen.strings +++ b/qmuidemo/en.lproj/LaunchScreen.strings @@ -1,3 +1,3 @@ /* Class = "UILabel"; text = "© 2018 QMUI Team All Rights Reserved."; ObjectID = "v3q-1s-6u4"; */ -"v3q-1s-6u4.text" = "© 2019 QMUI Team All Rights Reserved."; +"v3q-1s-6u4.text" = "© 2024 QMUI Team All Rights Reserved."; diff --git a/qmuidemo/qmuidemo.entitlements b/qmuidemo/qmuidemo.entitlements new file mode 100644 index 00000000..13ce7b19 --- /dev/null +++ b/qmuidemo/qmuidemo.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + com.apple.security.network.client + + com.apple.security.personal-information.photos-library + + + diff --git a/qmuidemo/zh-Hans.lproj/Info.plist b/qmuidemo/zh-Hans.lproj/Info.plist index 0003a5ea..21c8d138 100644 --- a/qmuidemo/zh-Hans.lproj/Info.plist +++ b/qmuidemo/zh-Hans.lproj/Info.plist @@ -38,8 +38,6 @@ armv7 - UIStatusBarStyle - UIStatusBarStyleLightContent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -54,7 +52,22 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneDelegateClassName + AppDelegate + UISceneConfigurationName + Default Configuration + + + + diff --git a/qmuidemo/zh-Hans.lproj/LaunchScreen.strings b/qmuidemo/zh-Hans.lproj/LaunchScreen.strings index 7e1503ce..6940ef37 100644 --- a/qmuidemo/zh-Hans.lproj/LaunchScreen.strings +++ b/qmuidemo/zh-Hans.lproj/LaunchScreen.strings @@ -1,3 +1,3 @@ /* Class = "UILabel"; text = "© 2018 QMUI Team All Rights Reserved."; ObjectID = "v3q-1s-6u4"; */ -"v3q-1s-6u4.text" = "© 2019 QMUI Team All Rights Reserved."; +"v3q-1s-6u4.text" = "© 2024 QMUI Team All Rights Reserved.";